Prevent conversation settings screens from stacking when switching recipients.

This commit is contained in:
Jeffrey Starke 2026-06-24 13:41:18 -04:00 committed by jeffrey-signal
parent 52750e726a
commit 141a128429
No known key found for this signature in database
5 changed files with 37 additions and 58 deletions

View File

@ -607,14 +607,6 @@ class MainActivity :
}
}
LaunchedEffect(wrappedNavigator.scaffoldValue.primary) {
if (wrappedNavigator.scaffoldValue.primary == PaneAdaptedValue.Hidden &&
mainNavigationState.currentListLocation.isChatsTab
) {
mainNavigationViewModel.onChatsDetailPaneCollapsed()
}
}
val noEnterTransitionFactory = remember {
AppScaffoldAnimationStateFactory(
enabledStates = AppScaffoldNavigator.NavigationState.entries.filterNot {
@ -736,16 +728,14 @@ class MainActivity :
primaryContent = {
when (mainNavigationState.currentListLocation) {
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> {
if (mainNavigationViewModel.chatsBackStackEntries.isNotEmpty()) {
NavDisplay(
backStack = mainNavigationViewModel.chatsBackStackEntries,
onBack = { mainNavigationViewModel.popChatsDetailLocation() },
transitionSpec = TransitionSpecs.HorizontalSlide.transitionSpec,
popTransitionSpec = TransitionSpecs.HorizontalSlide.popTransitionSpec,
predictivePopTransitionSpec = TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec,
entryProvider = entryProvider { chatsNavEntries(convoTransitionState) }
)
}
NavDisplay(
backStack = mainNavigationViewModel.chatsBackStackEntries,
onBack = { mainNavigationViewModel.popChatsDetailLocation() },
transitionSpec = TransitionSpecs.HorizontalSlide.transitionSpec,
popTransitionSpec = TransitionSpecs.HorizontalSlide.popTransitionSpec,
predictivePopTransitionSpec = TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec,
entryProvider = entryProvider { chatsNavEntries(convoTransitionState) }
)
}
MainNavigationListLocation.CALLS -> {

View File

@ -33,7 +33,7 @@ class ChatsBackStack(savedStateHandle: SavedStateHandle) {
key = KEY,
saver = saver
) {
mutableStateListOf()
mutableStateListOf(MainNavigationDetailLocation.Empty)
}
val activeRecipientId: RecipientId?
@ -45,8 +45,8 @@ class ChatsBackStack(savedStateHandle: SavedStateHandle) {
}
}
val hasConversation: Boolean
get() = entries.any { it is MainNavigationDetailLocation.Conversation }
val isEmpty: Boolean
get() = entries.singleOrNull() is MainNavigationDetailLocation.Empty
/**
* Pushes an entry onto the stack.
@ -76,21 +76,10 @@ class ChatsBackStack(savedStateHandle: SavedStateHandle) {
/**
* Resets the stack to its base empty state.
*/
fun reset(isSplitPane: Boolean) {
entries.clear()
if (isSplitPane) {
fun reset() {
entries.removeAll { it !is MainNavigationDetailLocation.Empty }
if (entries.isEmpty()) {
entries.add(MainNavigationDetailLocation.Empty)
}
}
/**
* Ensures that [MainNavigationDetailLocation.Empty] is present in the stack iff isSplitPane=true.
*/
fun updateEmptyDetailForPaneMode(isSplitPane: Boolean) {
val hasEmptyBase = entries.firstOrNull() is MainNavigationDetailLocation.Empty
when {
isSplitPane && !hasEmptyBase -> entries.add(0, MainNavigationDetailLocation.Empty)
!isSplitPane && hasEmptyBase -> entries.removeAll { it is MainNavigationDetailLocation.Empty }
}
}
}

View File

@ -20,7 +20,7 @@ object ConversationSettingsNavigator {
recipient: Recipient
) {
if (activity is MainNavigationChatDetailRouter) {
activity.goToChatDetail(MainNavigationDetailLocation.Chats.ConversationSettings(recipient.id, isContentRoot = true))
activity.goToChatDetail(MainNavigationDetailLocation.Chats.ConversationSettings(recipient.id))
return
}

View File

@ -92,9 +92,12 @@ sealed interface MainNavigationDetailLocation : Parcelable, NavKey {
@Serializable
data class ConversationSettings(
val recipientId: RecipientId,
override val isContentRoot: Boolean = false
val recipientId: RecipientId
) : Chats {
@Transient
@IgnoredOnParcel
override val isContentRoot: Boolean = false
@Transient
@IgnoredOnParcel
override val controllerKey: RecipientId = recipientId

View File

@ -6,7 +6,6 @@
package org.thoughtcrime.securesms.main
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
@ -172,15 +171,12 @@ class MainNavigationViewModel(
fun onSplitPaneChanged(isSplitPane: Boolean) {
this@MainNavigationViewModel.isSplitPane = isSplitPane
chatsBackStack.updateEmptyDetailForPaneMode(isSplitPane)
// if no conversation is selected, clear the empty detail pane when switching from split pane to single pane mode.
if (!isSplitPane &&
internalMainNavigationState.value.currentListLocation.isChatsTab &&
!chatsBackStack.hasConversation &&
navigator?.scaffoldValue?.primary == PaneAdaptedValue.Expanded
) {
navigatorScope?.launch { navigator?.navigateBack() }
if (!isSplitPane) {
if (chatsBackStack.isEmpty) {
lockPaneToSecondary = true
setFocusedPane(ThreePaneScaffoldRole.Secondary)
}
}
}
@ -295,7 +291,7 @@ class MainNavigationViewModel(
val currentListLocation = internalMainNavigationState.value.currentListLocation
when (location) {
is MainNavigationDetailLocation.Empty if currentListLocation.isChatsTab -> chatsBackStack.reset(isSplitPane)
is MainNavigationDetailLocation.Empty if currentListLocation.isChatsTab -> clearDetailLocation(chatsBackStack)
is MainNavigationDetailLocation.Chats -> pushChatsDetailLocation(location)
is MainNavigationDetailLocation.Conversation -> goToConversation(location)
@ -329,26 +325,27 @@ class MainNavigationViewModel(
}
private fun pushChatsDetailLocation(location: MainNavigationDetailLocation) {
if (location is MainNavigationDetailLocation.Chats && chatsBackStack.activeRecipientId != location.controllerKey) {
chatsBackStack.reset()
}
chatsBackStack.push(location)
updateActiveStateForLocation(location)
setFocusedPane(ThreePaneScaffoldRole.Primary)
}
/**
* Inverse of [pushChatsDetailLocation]. Pops the top chats detail entry and, if no conversation
* remains, records the user's intent to stay on the list pane (so a subsequent config change does
* not errantly restore them to the Primary/detail pane).
*/
fun popChatsDetailLocation() {
chatsBackStack.pop()
if (!chatsBackStack.hasConversation) {
if (chatsBackStack.isEmpty) {
lockPaneToSecondary = true
setFocusedPane(ThreePaneScaffoldRole.Secondary)
}
}
fun onChatsDetailPaneCollapsed() {
if (!chatsBackStack.hasConversation) {
chatsBackStack.reset(isSplitPane)
private fun clearDetailLocation(backStack: ChatsBackStack) {
backStack.reset()
if (!isSplitPane) {
lockPaneToSecondary = true
setFocusedPane(ThreePaneScaffoldRole.Secondary)
}
}