Remove custom WindowSizeClass and just depend on Material Adaptive WindowSizeClass.

Co-authored-by: jeffrey-signal <jeffrey@signal.org>
This commit is contained in:
Alex Hart
2025-10-31 12:50:33 -03:00
committed by jeffrey-signal
parent 95c9776b4d
commit 109f651681
32 changed files with 202 additions and 246 deletions

View File

@@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.PaneExpansionAnchor
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
@@ -62,6 +63,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
@@ -163,7 +166,8 @@ import org.thoughtcrime.securesms.window.AppPaneDragHandle
import org.thoughtcrime.securesms.window.AppScaffold
import org.thoughtcrime.securesms.window.AppScaffoldAnimationStateFactory
import org.thoughtcrime.securesms.window.AppScaffoldNavigator
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.Navigation
import org.thoughtcrime.securesms.window.isSplitPane
import org.thoughtcrime.securesms.window.rememberThreePaneScaffoldNavigatorDelegate
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
@@ -339,17 +343,23 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
)
}
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val contentLayoutData = MainContentLayoutData.rememberContentLayoutData(mainToolbarState.mode)
MainContainer {
val wrappedNavigator = rememberNavigator(windowSizeClass, contentLayoutData, maxWidth)
val listPaneWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
val navigation = Navigation.rememberNavigation()
val anchors = remember(contentLayoutData, mainToolbarState) {
val halfPartitionWidth = contentLayoutData.partitionWidth / 2
val detailOffset = if (mainToolbarState.mode == MainToolbarMode.SEARCH) 0.dp else 80.dp
val detailOffset = when {
mainToolbarState.mode == MainToolbarMode.SEARCH -> 0.dp
navigation == Navigation.BAR -> 0.dp
else -> 80.dp
}
val detailOnlyAnchor = PaneExpansionAnchor.Offset.fromStart(detailOffset + contentLayoutData.listPaddingStart + halfPartitionWidth)
val detailAndListAnchor = PaneExpansionAnchor.Offset.fromStart(listPaneWidth + halfPartitionWidth)
val listOnlyAnchor = PaneExpansionAnchor.Offset.fromEnd(contentLayoutData.detailPaddingEnd - halfPartitionWidth)
@@ -519,7 +529,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
},
secondaryContent = {
val listContainerColor = if (windowSizeClass.isMedium()) {
val listContainerColor = if (windowSizeClass.isSplitPane() && windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM) {
SignalTheme.colors.colorSurface1
} else {
MaterialTheme.colorScheme.surface
@@ -699,10 +709,10 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
@Composable
private fun MainContainer(content: @Composable BoxWithConstraintsScope.() -> Unit) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(this)) {
val backgroundColor = if (windowSizeClass.isCompact()) {
val backgroundColor = if (!windowSizeClass.isSplitPane()) {
MaterialTheme.colorScheme.surface
} else {
SignalTheme.colors.colorSurface1

View File

@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -47,7 +48,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsViewModel
import org.thoughtcrime.securesms.compose.ComposeDialogFragment
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.isSplitPane
class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
@@ -109,7 +110,7 @@ fun EditCallLinkNameScreen(
onNavigationClick = {
backPressedDispatcherOwner?.onBackPressedDispatcher?.onBackPressed()
},
showNavigationIcon = !WindowSizeClass.rememberWindowSizeClass().isSplitPane()
showNavigationIcon = !currentWindowAdaptiveInfo().windowSizeClass.isSplitPane()
)
}

View File

@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -55,7 +56,7 @@ import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
import org.thoughtcrime.securesms.sharing.v2.ShareActivity
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.isSplitPane
import java.time.Instant
@Composable
@@ -84,7 +85,7 @@ fun CallLinkDetailsScreen(
state = state,
showAlreadyInACall = showAlreadyInACall,
callback = callback,
showNavigationIcon = !WindowSizeClass.rememberWindowSizeClass().isSplitPane()
showNavigationIcon = !currentWindowAdaptiveInfo().windowSizeClass.isSplitPane()
)
}

View File

@@ -61,7 +61,8 @@ import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.doAfterNextLayout
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.visible
import org.thoughtcrime.securesms.window.WindowSizeClass.Companion.getWindowSizeClass
import org.thoughtcrime.securesms.window.getWindowSizeClass
import org.thoughtcrime.securesms.window.isSplitPane
import java.util.Objects
/**
@@ -184,7 +185,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
}
}
if (resources.getWindowSizeClass().isCompact()) {
if (!resources.getWindowSizeClass().isSplitPane()) {
ViewUtil.setBottomMargin(binding.bottomActionBar, ViewUtil.getNavigationBarHeight(binding.bottomActionBar))
}

View File

@@ -19,8 +19,9 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.WindowUtil
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.WindowSizeClass.Companion.getWindowSizeClass
import org.thoughtcrime.securesms.window.getWindowSizeClass
import org.thoughtcrime.securesms.window.isLargeScreenSupportEnabled
import org.thoughtcrime.securesms.window.isSplitPane
import com.google.android.material.R as MaterialR
/**
@@ -32,7 +33,7 @@ abstract class FixedRoundedCornerBottomSheetDialogFragment : BottomSheetDialogFr
* Sheet corner radius in DP
*/
protected val cornerRadius: Int by lazy {
if (WindowSizeClass.isLargeScreenSupportEnabled() && resources.getWindowSizeClass().isSplitPane()) {
if (isLargeScreenSupportEnabled() && resources.getWindowSizeClass().isSplitPane()) {
32
} else {
18

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components
import android.content.Context
import android.content.res.Configuration
import android.util.AttributeSet
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
@@ -15,7 +16,6 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.main.VerticalInsets
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.window.WindowSizeClass.Companion.getWindowSizeClass
import kotlin.math.roundToInt
/**
@@ -241,7 +241,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
}
private fun isLandscape(): Boolean {
return resources.getWindowSizeClass().isLandscape()
return resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}
private val Guideline?.guidelineEnd: Int

View File

@@ -8,10 +8,12 @@ package org.thoughtcrime.securesms.components.compose
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.thoughtcrime.securesms.window.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import org.thoughtcrime.securesms.window.isAtLeast
/**
* Displays the screen title for split-pane UIs on tablets and foldable devices.
@@ -21,7 +23,7 @@ fun ScreenTitlePane(
title: String,
modifier: Modifier = Modifier
) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
Text(
text = title,
@@ -29,7 +31,7 @@ fun ScreenTitlePane(
color = MaterialTheme.colorScheme.onSurface,
modifier = modifier
.padding(
start = if (windowSizeClass.isExtended()) 80.dp else 20.dp,
start = if (windowSizeClass.windowWidthSizeClass.isAtLeast(WindowWidthSizeClass.EXPANDED)) 80.dp else 20.dp,
end = 20.dp,
bottom = 12.dp
)

View File

@@ -185,11 +185,11 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
switchPref(
isEnabled = state.largeScreenUi,
title = DSLSettingsText.from("Force split pane UI on landscape phones."),
title = DSLSettingsText.from("Force split pane UI on phones."),
summary = DSLSettingsText.from("This setting requires split pane UI to be enabled."),
isChecked = state.forceSplitPaneOnCompactLandscape,
isChecked = state.forceSplitPane,
onClick = {
viewModel.setForceSplitPaneOnCompactLandscape(!state.forceSplitPaneOnCompactLandscape)
viewModel.setForceSplitPane(!state.forceSplitPane)
}
)

View File

@@ -32,5 +32,5 @@ data class InternalSettingsState(
val hevcEncoding: Boolean,
val newCallingUi: Boolean,
val largeScreenUi: Boolean,
val forceSplitPaneOnCompactLandscape: Boolean
val forceSplitPane: Boolean
)

View File

@@ -198,7 +198,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
hevcEncoding = SignalStore.internal.hevcEncoding,
newCallingUi = SignalStore.internal.newCallingUi,
largeScreenUi = SignalStore.internal.largeScreenUi,
forceSplitPaneOnCompactLandscape = SignalStore.internal.forceSplitPaneOnCompactLandscape
forceSplitPane = SignalStore.internal.forceSplitPane
)
fun onClearOnboardingState() {
@@ -219,8 +219,8 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}
fun setForceSplitPaneOnCompactLandscape(forceSplitPaneOnCompactLandscape: Boolean) {
SignalStore.internal.forceSplitPaneOnCompactLandscape = forceSplitPaneOnCompactLandscape
fun setForceSplitPane(forceSplitPane: Boolean) {
SignalStore.internal.forceSplitPane = forceSplitPane
refresh()
}

View File

@@ -37,6 +37,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -63,6 +64,8 @@ import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.BottomSheets
@@ -76,7 +79,6 @@ import org.thoughtcrime.securesms.events.CallParticipant
import org.thoughtcrime.securesms.events.GroupCallReactionEvent
import org.thoughtcrime.securesms.events.WebRtcViewModel
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.window.WindowSizeClass
import kotlin.math.max
import kotlin.math.round
import kotlin.time.Duration.Companion.seconds
@@ -484,7 +486,7 @@ private fun TinyLocalVideoRenderer(
if (LocalInspectionMode.current) {
Text(
"Test ${WindowSizeClass.rememberWindowSizeClass()}",
"Test ${currentWindowAdaptiveInfo().windowSizeClass}",
modifier = modifier
.padding(padding)
.height(height)
@@ -553,27 +555,28 @@ private fun SmallMoveableLocalVideoRenderer(
@Composable
private fun rememberTinyPortraitSize(): SelfPictureInPictureDimensions {
val smallWidth = dimensionResource(R.dimen.call_screen_overflow_item_size)
val windowClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
val smallSize = when (windowClass) {
WindowSizeClass.COMPACT_PORTRAIT -> DpSize(40.dp, smallWidth)
WindowSizeClass.COMPACT_LANDSCAPE -> DpSize(smallWidth, 40.dp)
WindowSizeClass.EXTENDED_PORTRAIT, WindowSizeClass.EXTENDED_LANDSCAPE -> DpSize(124.dp, 217.dp)
val smallSize = when {
windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT && !isLandscape -> DpSize(40.dp, smallWidth)
windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT && isLandscape -> DpSize(smallWidth, 40.dp)
windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED -> DpSize(124.dp, 217.dp)
else -> DpSize(smallWidth, smallWidth)
}
val expandedSize = when (windowClass) {
WindowSizeClass.COMPACT_PORTRAIT -> DpSize(180.dp, 320.dp)
WindowSizeClass.COMPACT_LANDSCAPE -> DpSize(320.dp, 180.dp)
val expandedSize = when {
windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT && !isLandscape -> DpSize(180.dp, 320.dp)
windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT && isLandscape -> DpSize(320.dp, 180.dp)
else -> DpSize(smallWidth, smallWidth)
}
val padding = when (windowClass) {
WindowSizeClass.COMPACT_PORTRAIT -> PaddingValues(vertical = 16.dp)
val padding = when {
windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT && !isLandscape -> PaddingValues(vertical = 16.dp)
else -> PaddingValues(16.dp)
}
return remember(windowClass) {
return remember(windowSizeClass) {
SelfPictureInPictureDimensions(smallSize, expandedSize, padding)
}
}

View File

@@ -26,6 +26,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -64,7 +65,8 @@ import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.window.AppScaffold
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.detailPaneMaxContentWidth
import org.thoughtcrime.securesms.window.isSplitPane
import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
/**
@@ -200,8 +202,8 @@ private fun NewConversationScreenUi(
uiState: NewConversationUiState,
callbacks: UiCallbacks
) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPaneOnCompactLandscape = uiState.forceSplitPaneOnCompactLandscape)
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPane = uiState.forceSplitPaneOnCompactLandscape)
val snackbarHostState = remember { SnackbarHostState() }
AppScaffold(

View File

@@ -161,7 +161,7 @@ class NewConversationViewModel : ViewModel() {
}
data class NewConversationUiState(
val forceSplitPaneOnCompactLandscape: Boolean = SignalStore.internal.forceSplitPaneOnCompactLandscape,
val forceSplitPaneOnCompactLandscape: Boolean = SignalStore.internal.forceSplitPane,
val searchQuery: String = "",
val isLookingUpRecipient: Boolean = false,
val isRefreshingContacts: Boolean = false,

View File

@@ -6,6 +6,8 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.DimensionUnit
import org.signal.core.util.concurrent.LifecycleDisposable
@@ -15,7 +17,6 @@ import org.thoughtcrime.securesms.components.ComposeText
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerFragmentV2
import org.thoughtcrime.securesms.util.adapter.mapping.AnyMappingModel
import org.thoughtcrime.securesms.util.doOnEachLayout
import org.thoughtcrime.securesms.window.WindowSizeClass
/**
* Controller for inline search results.
@@ -71,7 +72,7 @@ class InlineQueryResultsControllerV2(
}
fun onWindowSizeClassChanged(windowSizeClass: WindowSizeClass) {
this.shouldHideForWindowSizeClass = windowSizeClass == WindowSizeClass.COMPACT_LANDSCAPE
this.shouldHideForWindowSizeClass = windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT
if (shouldHideForWindowSizeClass) {
dismiss()

View File

@@ -24,7 +24,7 @@ import org.thoughtcrime.securesms.jobs.ConversationShortcutUpdateJob
import org.thoughtcrime.securesms.util.ConfigurationUtil
import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.isLargeScreenSupportEnabled
import java.util.concurrent.TimeUnit
/**
@@ -54,7 +54,7 @@ open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaCo
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
if (!ActivityCompat.isLaunchedFromBubble(this) && WindowSizeClass.isLargeScreenSupportEnabled()) {
if (!ActivityCompat.isLaunchedFromBubble(this) && isLargeScreenSupportEnabled()) {
startActivity(
MainActivity.clearTop(this).apply {
action = ConversationIntents.ACTION

View File

@@ -361,8 +361,9 @@ import org.thoughtcrime.securesms.util.visible
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.WindowSizeClass.Companion.getWindowSizeClass
import org.thoughtcrime.securesms.window.getWindowSizeClass
import org.thoughtcrime.securesms.window.isLargeScreenSupportEnabled
import org.thoughtcrime.securesms.window.isSplitPane
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
@@ -615,8 +616,8 @@ class ConversationFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.toolbar.isBackInvokedCallbackEnabled = false
binding.root.setApplyRootInsets(!WindowSizeClass.isLargeScreenSupportEnabled())
binding.root.setUseWindowTypes(!WindowSizeClass.isLargeScreenSupportEnabled())
binding.root.setApplyRootInsets(!isLargeScreenSupportEnabled())
binding.root.setUseWindowTypes(!isLargeScreenSupportEnabled())
disposables.bindTo(viewLifecycleOwner)
@@ -699,7 +700,7 @@ class ConversationFragment :
override fun onResume() {
super.onResume()
if (!WindowSizeClass.isLargeScreenSupportEnabled()) {
if (!isLargeScreenSupportEnabled()) {
WindowUtil.setLightNavigationBarFromTheme(requireActivity())
WindowUtil.setLightStatusBarFromTheme(requireActivity())
}
@@ -1455,7 +1456,7 @@ class ConversationFragment :
}
private fun presentNavigationIconForNormal() {
if (WindowSizeClass.isLargeScreenSupportEnabled()) {
if (isLargeScreenSupportEnabled()) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mainNavigationViewModel.isFullScreenPane.collect { isFullScreenPane ->
@@ -3483,7 +3484,7 @@ class ConversationFragment :
getVoiceNoteMediaController().resumePlayback(selectedConversationModel.audioUri, messageRecord.id)
}
if (!WindowSizeClass.isLargeScreenSupportEnabled()) {
if (!isLargeScreenSupportEnabled()) {
WindowUtil.setLightStatusBarFromTheme(requireActivity())
WindowUtil.setLightNavigationBarFromTheme(requireActivity())
}

View File

@@ -53,6 +53,7 @@ import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
import androidx.window.core.layout.WindowHeightSizeClass;
import com.airbnb.lottie.SimpleColorFilter;
import com.annimon.stream.Stream;
@@ -159,7 +160,6 @@ import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.thoughtcrime.securesms.window.WindowSizeClass;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
import java.lang.ref.WeakReference;
@@ -179,6 +179,9 @@ import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
import kotlin.Unit;
import static org.thoughtcrime.securesms.window.WindowSizeClassExtensionsKt.getWindowSizeClass;
import static org.thoughtcrime.securesms.window.WindowSizeClassExtensionsKt.isSplitPane;
public class ConversationListFragment extends MainFragment implements ConversationListAdapter.OnConversationClickListener,
ClearFilterViewHolder.OnClearFilterClickListener,
@@ -311,7 +314,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
searchAdapter = contactSearchMediator.getAdapter();
if (WindowSizeClass.Companion.getWindowSizeClass(getResources()).isCompact()) {
if (getWindowSizeClass(getResources()).getWindowHeightSizeClass() == WindowHeightSizeClass.COMPACT) {
ViewUtil.setBottomMargin(bottomActionBar, ViewUtil.getNavigationBarHeight(bottomActionBar));
}
@@ -411,7 +414,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
}
}));
if (WindowSizeClass.Companion.getWindowSizeClass(getResources()).isSplitPane()) {
if (isSplitPane(getWindowSizeClass(getResources()))) {
lifecycleDisposable.add(mainNavigationViewModel.getObservableActiveChatThreadId()
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(defaultAdapter::setActiveThreadId));

View File

@@ -16,7 +16,8 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
import org.thoughtcrime.securesms.window.WindowSizeClass.Companion.getWindowSizeClass
import org.thoughtcrime.securesms.window.getWindowSizeClass
import org.thoughtcrime.securesms.window.isSplitPane
/**
* When the user searches for a conversation and then enters a message, we should clear
@@ -35,7 +36,7 @@ fun Fragment.listenToEventBusWhileResumed(
detailLocation
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.RESUMED)
.collectLatest {
if (resources.getWindowSizeClass().isCompact()) {
if (!resources.getWindowSizeClass().isSplitPane()) {
when (it) {
is MainNavigationDetailLocation.Chats.Conversation -> unsubscribe()
MainNavigationDetailLocation.Empty -> subscribe()

View File

@@ -33,6 +33,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -68,7 +69,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode
import org.thoughtcrime.securesms.window.AppScaffold
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.detailPaneMaxContentWidth
import org.thoughtcrime.securesms.window.isSplitPane
import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
import java.text.NumberFormat
@@ -162,8 +164,8 @@ private fun CreateGroupScreenUi(
uiState: CreateGroupUiState,
callbacks: UiCallbacks
) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPaneOnCompactLandscape = uiState.forceSplitPaneOnCompactLandscape)
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPane = uiState.forceSplitPane)
val snackbarHostState = remember { SnackbarHostState() }
val titleText = if (uiState.newSelections.isNotEmpty()) {
@@ -352,7 +354,7 @@ private fun CreateGroupScreenPreview() {
Previews.Preview {
CreateGroupScreenUi(
uiState = CreateGroupUiState(
forceSplitPaneOnCompactLandscape = false,
forceSplitPane = false,
selectionLimits = SelectionLimits.NO_LIMITS
),
callbacks = UiCallbacks.Empty

View File

@@ -168,7 +168,7 @@ class CreateGroupViewModel : ViewModel() {
}
data class CreateGroupUiState(
val forceSplitPaneOnCompactLandscape: Boolean = SignalStore.internal.forceSplitPaneOnCompactLandscape,
val forceSplitPane: Boolean = SignalStore.internal.forceSplitPane,
val searchQuery: String = "",
val selectionLimits: SelectionLimits = RemoteConfig.groupLimits.excludingSelf(),
val newSelections: List<SelectedContact> = emptyList(),

View File

@@ -51,7 +51,7 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal
/**
* Force split-pane mode on compact landscape
*/
var forceSplitPaneOnCompactLandscape by booleanValue(FORCE_SPLIT_PANE_ON_COMPACT_LANDSCAPE, false).falseForExternalUsers()
var forceSplitPane by booleanValue(FORCE_SPLIT_PANE_ON_COMPACT_LANDSCAPE, false).falseForExternalUsers()
/**
* Members will not be added directly to a GV2 even if they could be.

View File

@@ -34,13 +34,14 @@ import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.window.WindowSizeClass;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Locale;
import static org.thoughtcrime.securesms.window.WindowSizeClassExtensionsKt.getWindowSizeClass;
public class LogSectionSystemInfo implements LogSection {
@Override
@@ -62,7 +63,7 @@ public class LogSectionSystemInfo implements LogSection {
builder.append("Screen : ").append(getScreenResolution(context)).append(", ")
.append(ScreenDensity.get(context)).append(", ")
.append(getScreenRefreshRate(context)).append("\n");
builder.append("WindowSizeClass : ").append(WindowSizeClass.Companion.getWindowSizeClass(context.getResources())).append("\n");
builder.append("WindowSizeClass : ").append(getWindowSizeClass(context.getResources())).append("\n");
builder.append("Font Scale : ").append(context.getResources().getConfiguration().fontScale).append("\n");
builder.append("Animation Scale : ").append(ContextUtil.getAnimationScale(context)).append("\n");
builder.append("Android : ").append(Build.VERSION.RELEASE).append(", API ")

View File

@@ -39,6 +39,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import androidx.window.core.layout.WindowSizeClass
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@@ -49,7 +50,8 @@ import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
import org.thoughtcrime.securesms.serialization.JsonSerializableNavType
import org.thoughtcrime.securesms.window.AppScaffoldAnimationDefaults
import org.thoughtcrime.securesms.window.AppScaffoldAnimationState
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.isLargeScreenSupportEnabled
import org.thoughtcrime.securesms.window.isSplitPane
import kotlin.reflect.typeOf
import kotlin.time.Duration.Companion.milliseconds
@@ -205,7 +207,7 @@ class ChatNavGraphState private constructor(
private var hasWrittenToGraphicsLayer: Boolean by mutableStateOf(false)
suspend fun writeGraphicsLayerToBitmap() {
if (WindowSizeClass.isLargeScreenSupportEnabled() && !windowSizeClass.isSplitPane() && hasWrittenToGraphicsLayer) {
if (isLargeScreenSupportEnabled() && !windowSizeClass.isSplitPane() && hasWrittenToGraphicsLayer) {
chatBitmap = graphicsLayer.toImageBitmap()
}
}

View File

@@ -15,12 +15,12 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.megaphone.Megaphone
import org.thoughtcrime.securesms.megaphone.MegaphoneActionController
import org.thoughtcrime.securesms.megaphone.Megaphones
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.isSplitPane
data class SnackbarState(
val message: String,
@@ -77,14 +77,14 @@ fun MainBottomChrome(
megaphoneActionController: MegaphoneActionController,
modifier: Modifier = Modifier
) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
Column(
modifier = modifier
.fillMaxWidth()
.animateContentSize()
) {
if (state.mainToolbarMode == MainToolbarMode.FULL && windowSizeClass.isCompact()) {
if (state.mainToolbarMode == MainToolbarMode.FULL && !windowSizeClass.isSplitPane()) {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier.fillMaxWidth()
@@ -104,7 +104,7 @@ fun MainBottomChrome(
)
}
val snackBarModifier = if (windowSizeClass.isCompact() && state.mainToolbarMode == MainToolbarMode.BASIC) {
val snackBarModifier = if (!windowSizeClass.isSplitPane() && state.mainToolbarMode == MainToolbarMode.BASIC) {
Modifier.navigationBarsPadding()
} else {
Modifier

View File

@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.main
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
@@ -13,7 +14,9 @@ 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
import androidx.window.core.layout.WindowWidthSizeClass
import org.thoughtcrime.securesms.window.isAtLeast
import org.thoughtcrime.securesms.window.isSplitPane
private val MEDIUM_CONTENT_CORNERS = 18.dp
private val EXTENDED_CONTENT_CORNERS = 14.dp
@@ -42,7 +45,7 @@ data class MainContentLayoutData(
*/
@Composable
fun hasDragHandle(): Boolean {
return !WindowSizeClass.rememberWindowSizeClass().isCompact()
return currentWindowAdaptiveInfo().windowSizeClass.isSplitPane()
}
/**
@@ -50,12 +53,12 @@ data class MainContentLayoutData(
*/
@Composable
fun rememberDefaultPanePreferredWidth(maxWidth: Dp): Dp {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
return remember(maxWidth, windowSizeClass) {
when {
!windowSizeClass.isSplitPane() -> maxWidth
windowSizeClass.isExtended() -> 416.dp
windowSizeClass.windowWidthSizeClass.isAtLeast(WindowWidthSizeClass.EXPANDED) -> 416.dp
else -> (maxWidth - extraPadding) / 2f
}
}
@@ -67,23 +70,23 @@ data class MainContentLayoutData(
*/
@Composable
fun rememberContentLayoutData(mode: MainToolbarMode): MainContentLayoutData {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
return remember(windowSizeClass, mode) {
MainContentLayoutData(
shape = when {
!windowSizeClass.isSplitPane() -> RectangleShape
windowSizeClass.isExtended() -> RoundedCornerShape(EXTENDED_CONTENT_CORNERS)
windowSizeClass.windowWidthSizeClass.isAtLeast(WindowWidthSizeClass.EXPANDED) -> 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)
windowSizeClass.windowWidthSizeClass.isAtLeast(WindowWidthSizeClass.EXPANDED) -> 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.isSplitPane() -> 0.dp
windowSizeClass.isExtended() -> 24.dp
windowSizeClass.windowWidthSizeClass.isAtLeast(WindowWidthSizeClass.EXPANDED) -> 24.dp
else -> 13.dp
},
listPaddingStart = when {
@@ -97,7 +100,7 @@ data class MainContentLayoutData(
},
detailPaddingEnd = when {
!windowSizeClass.isSplitPane() -> 0.dp
windowSizeClass.isExtended() -> 24.dp
windowSizeClass.windowWidthSizeClass.isAtLeast(WindowWidthSizeClass.EXPANDED) -> 24.dp
else -> 12.dp
}
)

View File

@@ -8,17 +8,18 @@ package org.thoughtcrime.securesms.main
import android.app.Activity
import android.content.Intent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.fragment.app.DialogFragment
import androidx.window.core.layout.WindowHeightSizeClass
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.megaphone.Megaphone
import org.thoughtcrime.securesms.megaphone.MegaphoneActionController
import org.thoughtcrime.securesms.megaphone.MegaphoneComponent
import org.thoughtcrime.securesms.megaphone.Megaphones
import org.thoughtcrime.securesms.window.WindowSizeClass
data class MainMegaphoneState(
val megaphone: Megaphone = Megaphone.NONE,
@@ -44,9 +45,9 @@ fun MainMegaphoneContainer(
controller: MegaphoneActionController,
onMegaphoneVisible: (Megaphone) -> Unit
) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val visible = remember(windowSizeClass, state) {
!(state.megaphone == Megaphone.NONE || state.mainToolbarMode != MainToolbarMode.FULL || windowSizeClass == WindowSizeClass.COMPACT_LANDSCAPE)
!(state.megaphone == Megaphone.NONE || state.mainToolbarMode != MainToolbarMode.FULL || windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT)
}
AnimatedVisibility(visible = visible) {
@@ -57,7 +58,7 @@ fun MainMegaphoneContainer(
}
LaunchedEffect(state, windowSizeClass) {
if (state.megaphone == Megaphone.NONE || state.mainToolbarMode == MainToolbarMode.BASIC || windowSizeClass == WindowSizeClass.COMPACT_LANDSCAPE) {
if (state.megaphone == Megaphone.NONE || state.mainToolbarMode == MainToolbarMode.BASIC || windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT) {
return@LaunchedEffect
}

View File

@@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.megaphone.Megaphones
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.window.AppScaffoldNavigator
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.isLargeScreenSupportEnabled
import java.util.Optional
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@@ -198,7 +198,7 @@ class MainNavigationViewModel(
override fun goTo(location: MainNavigationDetailLocation) {
lockPaneToSecondary = false
if (!WindowSizeClass.isLargeScreenSupportEnabled()) {
if (!isLargeScreenSupportEnabled()) {
goToLegacyDetailLocation?.invoke(location)
return
}

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
@@ -20,7 +21,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import kotlinx.parcelize.Parcelize
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.isSplitPane
@Parcelize
data class VerticalInsets(
@@ -41,7 +42,7 @@ fun rememberVerticalInsets(): State<VerticalInsets> {
val navigationBarPadding = navigationBarInsets.asPaddingValues()
val density = LocalDensity.current
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val insets = rememberSaveable { mutableStateOf(VerticalInsets.Zero) }
val updated = remember(statusBarInsets, navigationBarInsets, windowSizeClass) {

View File

@@ -5,8 +5,6 @@
package org.thoughtcrime.securesms.window
import android.content.res.Configuration
import android.content.res.Resources
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.background
@@ -16,7 +14,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -42,23 +39,17 @@ import androidx.compose.ui.draw.drawWithContent
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.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
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.main.MainFloatingActionButtonsCallback
import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationRail
import org.thoughtcrime.securesms.main.MainNavigationState
import org.thoughtcrime.securesms.util.RemoteConfig
import kotlin.math.max
enum class Navigation {
@@ -68,141 +59,15 @@ enum class Navigation {
companion object {
@Composable
fun rememberNavigation(): Navigation {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
return remember(windowSizeClass) { windowSizeClass.navigation }
}
}
}
/**
* Describes the size of screen we are displaying, and what components should be displayed.
*
* Screens should utilize this class by convention instead of calling [currentWindowAdaptiveInfo]
* themselves, as this class includes checks with [RemoteConfig] to ensure we're allowed to display
* content in different screen sizes.
*
* https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes
*/
enum class WindowSizeClass(
val navigation: Navigation
) {
COMPACT_PORTRAIT(Navigation.BAR),
COMPACT_LANDSCAPE(Navigation.BAR),
MEDIUM_PORTRAIT(Navigation.RAIL),
MEDIUM_LANDSCAPE(Navigation.RAIL),
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
fun isLandscape(): Boolean = this == COMPACT_LANDSCAPE || this == MEDIUM_LANDSCAPE || this == EXTENDED_LANDSCAPE
fun isPortrait(): Boolean = !isLandscape()
@JvmOverloads
fun isSplitPane(
forceSplitPaneOnCompactLandscape: Boolean = SignalStore.internal.forceSplitPaneOnCompactLandscape
): Boolean {
return if (isLargeScreenSupportEnabled() && forceSplitPaneOnCompactLandscape) {
this != COMPACT_PORTRAIT
} else {
this.navigation != Navigation.BAR
}
}
companion object {
@OptIn(ExperimentalWindowCoreApi::class)
fun Resources.getWindowSizeClass(): WindowSizeClass {
val orientation = configuration.orientation
if (isForcedCompact()) {
return getCompactSizeClassForOrientation(orientation)
}
val windowSizeClass = androidx.window.core.layout.WindowSizeClass.compute(
displayMetrics.widthPixels,
displayMetrics.heightPixels,
displayMetrics.density
)
return getSizeClassForOrientationAndSystemSizeClass(orientation, windowSizeClass)
}
fun isLargeScreenSupportEnabled(): Boolean {
return RemoteConfig.largeScreenUi && SignalStore.internal.largeScreenUi
}
fun isForcedCompact(): Boolean {
return !isLargeScreenSupportEnabled()
}
@Composable
fun checkForcedCompact(): Boolean {
return !LocalInspectionMode.current && isForcedCompact()
}
@Composable
fun rememberWindowSizeClass(forceCompact: Boolean = checkForcedCompact()): WindowSizeClass {
val orientation = LocalConfiguration.current.orientation
if (forceCompact) {
return remember(orientation) {
getCompactSizeClassForOrientation(orientation)
return remember(windowSizeClass) {
if (windowSizeClass.isSplitPane() && windowSizeClass.windowHeightSizeClass.isAtLeast(WindowHeightSizeClass.MEDIUM)) {
RAIL
} else {
BAR
}
}
val wsc = currentWindowAdaptiveInfo().windowSizeClass
return remember(orientation, wsc) {
getSizeClassForOrientationAndSystemSizeClass(orientation, wsc)
}
}
private fun getCompactSizeClassForOrientation(orientation: Int): WindowSizeClass {
return when (orientation) {
Configuration.ORIENTATION_PORTRAIT, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_SQUARE -> {
COMPACT_PORTRAIT
}
Configuration.ORIENTATION_LANDSCAPE -> COMPACT_LANDSCAPE
else -> error("Unexpected orientation: $orientation")
}
}
private fun getSizeClassForOrientationAndSystemSizeClass(orientation: Int, windowSizeClass: androidx.window.core.layout.WindowSizeClass): WindowSizeClass {
if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT || windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT) {
return getCompactSizeClassForOrientation(orientation)
}
return when (orientation) {
Configuration.ORIENTATION_PORTRAIT, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_SQUARE -> {
when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> COMPACT_PORTRAIT
WindowWidthSizeClass.MEDIUM -> MEDIUM_PORTRAIT
WindowWidthSizeClass.EXPANDED -> EXTENDED_PORTRAIT
else -> error("Unsupported.")
}
}
Configuration.ORIENTATION_LANDSCAPE -> {
when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> COMPACT_LANDSCAPE
WindowWidthSizeClass.MEDIUM -> MEDIUM_LANDSCAPE
WindowWidthSizeClass.EXPANDED -> EXTENDED_LANDSCAPE
else -> error("Unsupported.")
}
}
else -> error("Unexpected orientation: $orientation")
}
}
}
}
@@ -240,8 +105,8 @@ fun AppScaffold(
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
animatorFactory: AppScaffoldAnimationStateFactory = AppScaffoldAnimationStateFactory.Default
) {
val isForcedCompact = WindowSizeClass.checkForcedCompact()
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val isForcedCompact = !LocalInspectionMode.current && !isLargeScreenSupportEnabled()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
if (isForcedCompact) {
ListAndNavigation(
@@ -249,7 +114,6 @@ fun AppScaffold(
listContent = secondaryContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
windowSizeClass = windowSizeClass,
contentWindowInsets = contentWindowInsets,
modifier = modifier
)
@@ -314,7 +178,6 @@ fun AppScaffold(
listContent = secondaryContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
windowSizeClass = windowSizeClass,
contentWindowInsets = WindowInsets() // parent scaffold already applies the necessary insets
)
}
@@ -380,10 +243,11 @@ private fun ListAndNavigation(
navRailContent: @Composable () -> Unit,
bottomNavContent: @Composable () -> Unit,
snackbarHost: @Composable () -> Unit = {},
windowSizeClass: WindowSizeClass,
contentWindowInsets: WindowInsets,
modifier: Modifier = Modifier
) {
val navigation = Navigation.rememberNavigation()
Scaffold(
containerColor = Color.Transparent,
topBar = topBarContent,
@@ -394,9 +258,8 @@ private fun ListAndNavigation(
Row(
modifier = Modifier
.padding(paddingValues)
.then(if (windowSizeClass.isLandscape()) Modifier.displayCutoutPadding() else Modifier)
) {
if (windowSizeClass.navigation == Navigation.RAIL) {
if (navigation == Navigation.RAIL) {
navRailContent()
}
@@ -405,7 +268,7 @@ private fun ListAndNavigation(
listContent()
}
if (windowSizeClass.navigation == Navigation.BAR) {
if (navigation == Navigation.BAR) {
bottomNavContent()
}
}
@@ -418,11 +281,11 @@ private fun ListAndNavigation(
@Composable
private fun AppScaffoldPreview() {
Previews.Preview {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
AppScaffold(
navigator = rememberAppScaffoldNavigator(
isSplitPane = windowSizeClass.navigation != Navigation.BAR,
isSplitPane = windowSizeClass.isSplitPane(),
defaultPanePreferredWidth = 416.dp,
horizontalPartitionSpacerSize = 16.dp
),

View File

@@ -21,6 +21,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.unit.Dp
import androidx.window.core.layout.WindowSizeClass
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
@@ -94,9 +95,9 @@ open class AppScaffoldNavigator<T> @RememberInComposition constructor(private va
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun rememberAppScaffoldNavigator(
windowSizeClass: WindowSizeClass = WindowSizeClass.rememberWindowSizeClass(),
windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass,
isSplitPane: Boolean = windowSizeClass.isSplitPane(
forceSplitPaneOnCompactLandscape = if (LocalInspectionMode.current) false else SignalStore.internal.forceSplitPaneOnCompactLandscape
forceSplitPane = if (LocalInspectionMode.current) false else SignalStore.internal.forceSplitPane
),
horizontalPartitionSpacerSize: Dp = windowSizeClass.horizontalPartitionDefaultSpacerSize,
defaultPanePreferredWidth: Dp = windowSizeClass.listPaneDefaultPreferredWidth

View File

@@ -12,6 +12,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -29,7 +30,7 @@ import org.thoughtcrime.securesms.R
@Composable
private fun AppScaffoldWithTopBarPreview() {
Previews.Preview {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
AppScaffold(
navigator = rememberAppScaffoldNavigator(),

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.window
import android.content.res.Resources
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.window.core.ExperimentalWindowCoreApi
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
val WindowSizeClass.listPaneDefaultPreferredWidth: Dp get() = if (windowWidthSizeClass.isAtLeast(WindowWidthSizeClass.EXPANDED)) 416.dp else 316.dp
val WindowSizeClass.horizontalPartitionDefaultSpacerSize: Dp get() = 12.dp
val WindowSizeClass.detailPaneMaxContentWidth: Dp get() = 624.dp
fun WindowHeightSizeClass.isAtLeast(other: WindowHeightSizeClass): Boolean {
return hashCode() >= other.hashCode()
}
fun WindowWidthSizeClass.isAtLeast(other: WindowWidthSizeClass): Boolean {
return hashCode() >= other.hashCode()
}
/**
* Global check for large screen support, can be inlined after production release.
*/
fun isLargeScreenSupportEnabled(): Boolean {
return RemoteConfig.largeScreenUi && SignalStore.internal.largeScreenUi
}
@OptIn(ExperimentalWindowCoreApi::class)
fun Resources.getWindowSizeClass(): WindowSizeClass {
return WindowSizeClass.compute(displayMetrics.widthPixels, displayMetrics.heightPixels, displayMetrics.density)
}
/**
* Split Pane is enabled as long as the width size class is MEDIUM or greater
*/
@JvmOverloads
fun WindowSizeClass.isSplitPane(
forceSplitPane: Boolean = SignalStore.internal.forceSplitPane
): Boolean {
if (forceSplitPane) {
return true
}
return windowWidthSizeClass.isAtLeast(WindowWidthSizeClass.MEDIUM)
}