From 141a1284297f41815e4d4e01367daa02ba770306 Mon Sep 17 00:00:00 2001 From: Jeffrey Starke Date: Wed, 24 Jun 2026 13:41:18 -0400 Subject: [PATCH] Prevent conversation settings screens from stacking when switching recipients. --- .../thoughtcrime/securesms/MainActivity.kt | 26 ++++--------- .../securesms/chats/ChatsBackStack.kt | 23 +++--------- .../ConversationSettingsNavigator.kt | 2 +- .../main/MainNavigationDetailLocation.kt | 7 +++- .../securesms/main/MainNavigationViewModel.kt | 37 +++++++++---------- 5 files changed, 37 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt index c315782a8a..7a19c4fb75 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt @@ -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 -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/chats/ChatsBackStack.kt b/app/src/main/java/org/thoughtcrime/securesms/chats/ChatsBackStack.kt index d7a0f1a35e..798654d50d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/chats/ChatsBackStack.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/chats/ChatsBackStack.kt @@ -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 } - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsNavigator.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsNavigator.kt index b454bcfcc0..9b4c1f04e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsNavigator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsNavigator.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationDetailLocation.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationDetailLocation.kt index ebf2e890e4..b01f75a7ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationDetailLocation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationDetailLocation.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt index 7cf11491ab..b531cfbdf5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt @@ -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) } }