fix(nav): back from a list-opened chat returns to that list (#1400)
This commit is contained in:
1 parent
57be85d2f7
commit
012440b8f7
3 files changed
+142
-8
No files matched your search
@@ -215,14 +215,22 @@ fun MainNavigation(sessionId: String? = null) {
|
||||
var lastBackPressTime by remember { mutableLongStateOf(0L) }
|
||||
val backToastText = stringResource(R.string.press_back_again_to_exit)
|
||||
|
||||
// Double-back to exit on root ChatScreen
|
||||
// Double-back to exit on root ChatScreen — unless the chat was opened from
|
||||
// a list (History, Bots): then the first back returns to that list (Telegram
|
||||
// chat-list parity) and only a further double-back exits.
|
||||
BackHandler(enabled = currentScreen == ChatScreen && backStack.size == 1) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastBackPressTime < 2000L) {
|
||||
(context as? Activity)?.finish()
|
||||
val pendingReturn = NavigationController.chatReturnScreen
|
||||
if (pendingReturn != null) {
|
||||
NavigationController.consumeChatReturnScreen()
|
||||
NavigationController.navigateTo(pendingReturn)
|
||||
} else {
|
||||
lastBackPressTime = now
|
||||
Toast.makeText(context, backToastText, Toast.LENGTH_SHORT).show()
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastBackPressTime < 2000L) {
|
||||
(context as? Activity)?.finish()
|
||||
} else {
|
||||
lastBackPressTime = now
|
||||
Toast.makeText(context, backToastText, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,16 @@ object NavigationController {
|
||||
private set
|
||||
var pendingNewChatNavigation: PendingNewChatNavigation? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
/**
|
||||
* The list screen a chat was opened from (History, Bots). Back out of that
|
||||
* chat returns here instead of exiting the app (Telegram chat-list parity).
|
||||
* Consumed by MainNavigation's root-chat back handler; cleared by any other
|
||||
* navigation so a stale target can never hijack a later back press.
|
||||
*/
|
||||
var chatReturnScreen: NavKey? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
val pendingSessionId: String? get() = pendingChatNavigation?.sessionId
|
||||
|
||||
private var nextChatNavigationRequestId = 0L
|
||||
@@ -45,8 +55,18 @@ object NavigationController {
|
||||
*/
|
||||
fun isPrimaryScreen(key: NavKey): Boolean = key == ChatScreen || ScreenRegistry.ALL_SCREENS.any { it.key == key }
|
||||
|
||||
/** Returns the armed return screen and disarms it. */
|
||||
fun consumeChatReturnScreen(): NavKey? = chatReturnScreen.also { chatReturnScreen = null }
|
||||
|
||||
fun navigateTo(key: NavKey) {
|
||||
val stack = backStack ?: return
|
||||
|
||||
// Any explicit navigation invalidates the pending chat return — only a
|
||||
// session/chat opened from a list arms it again (see [queueChatNavigation]).
|
||||
// This must precede the dedup guard: re-selecting Chat in the drawer is
|
||||
// still an explicit navigation and must not keep a stale return armed.
|
||||
chatReturnScreen = null
|
||||
|
||||
if (stack.lastOrNull() == key) return
|
||||
|
||||
if (key == ChatScreen) {
|
||||
@@ -73,11 +93,16 @@ object NavigationController {
|
||||
}
|
||||
|
||||
fun openChatSession(sessionId: String) {
|
||||
queueChatNavigation(sessionId, scrollToBottom = false)
|
||||
// Opened from a list (History cards, search results, Bots): arm the
|
||||
// return so back from the chat lands back on that list instead of
|
||||
// exiting the app (Telegram chat-list parity).
|
||||
queueChatNavigation(sessionId, scrollToBottom = false, returnToList = true)
|
||||
}
|
||||
|
||||
fun openChatSessionFromNotification(sessionId: String) {
|
||||
queueChatNavigation(sessionId, scrollToBottom = true)
|
||||
// A notification open is not a list navigation: back keeps the classic
|
||||
// root-chat exit flow.
|
||||
queueChatNavigation(sessionId, scrollToBottom = true, returnToList = false)
|
||||
}
|
||||
|
||||
fun openNewChat() {
|
||||
@@ -91,8 +116,18 @@ object NavigationController {
|
||||
private fun queueChatNavigation(
|
||||
sessionId: String,
|
||||
scrollToBottom: Boolean,
|
||||
returnToList: Boolean,
|
||||
) {
|
||||
if (sessionId.isBlank()) return
|
||||
// Snapshot the origin BEFORE navigateTo resets the stack to [ChatScreen].
|
||||
val returnScreen =
|
||||
if (returnToList) {
|
||||
backStack
|
||||
?.lastOrNull()
|
||||
?.takeIf { it != ChatScreen && isPrimaryScreen(it) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
pendingChatNavigation =
|
||||
PendingChatNavigation(
|
||||
sessionId = sessionId,
|
||||
@@ -100,6 +135,8 @@ object NavigationController {
|
||||
requestId = ++nextChatNavigationRequestId,
|
||||
)
|
||||
navigateTo(ChatScreen)
|
||||
// Set after navigateTo so the reset itself does not disarm the return.
|
||||
chatReturnScreen = returnScreen
|
||||
}
|
||||
|
||||
fun consumePendingChatNavigation(): PendingChatNavigation? =
|
||||
@@ -113,6 +150,7 @@ object NavigationController {
|
||||
/** Clear the stack and navigate to the given screen atomically. */
|
||||
fun resetTo(screen: NavKey) {
|
||||
val stack = backStack ?: return
|
||||
chatReturnScreen = null
|
||||
stack.clear()
|
||||
stack.add(screen)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class NavigationControllerTest {
|
||||
NavigationController.backStack = null
|
||||
NavigationController.consumePendingSessionId()
|
||||
NavigationController.consumePendingNewChatNavigation()
|
||||
NavigationController.consumeChatReturnScreen()
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -30,6 +31,7 @@ class NavigationControllerTest {
|
||||
NavigationController.backStack = null
|
||||
NavigationController.consumePendingSessionId()
|
||||
NavigationController.consumePendingNewChatNavigation()
|
||||
NavigationController.consumeChatReturnScreen()
|
||||
}
|
||||
|
||||
// ── Dedup guard: navigateTo with same key ──────────────────────────────
|
||||
@@ -171,6 +173,92 @@ class NavigationControllerTest {
|
||||
assertFalse(NavigationController.consumePendingChatNavigation()?.scrollToBottom == true)
|
||||
}
|
||||
|
||||
// ── Chat return screen: back from a list-opened chat returns to that list ──
|
||||
|
||||
@Test
|
||||
fun `openChatSession from a list arms the list as the return screen`() {
|
||||
NavigationController.backStack = NavBackStack<NavKey>(HistoryScreen)
|
||||
|
||||
NavigationController.openChatSession("stored-session")
|
||||
|
||||
assertEquals(ChatScreen, NavigationController.backStack?.lastOrNull())
|
||||
assertEquals(HistoryScreen, NavigationController.chatReturnScreen)
|
||||
assertEquals(HistoryScreen, NavigationController.consumeChatReturnScreen())
|
||||
assertNull("consume must disarm", NavigationController.consumeChatReturnScreen())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openChatSession from Bots arms Bots`() {
|
||||
NavigationController.backStack = NavBackStack<NavKey>(BotsScreen)
|
||||
|
||||
NavigationController.openChatSession("bot-session")
|
||||
|
||||
assertEquals(BotsScreen, NavigationController.consumeChatReturnScreen())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openChatSession from chat arms nothing`() {
|
||||
NavigationController.backStack = NavBackStack<NavKey>(ChatScreen)
|
||||
|
||||
NavigationController.openChatSession("stored-session")
|
||||
|
||||
assertNull(NavigationController.consumeChatReturnScreen())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `notification open never arms a return screen`() {
|
||||
NavigationController.backStack = NavBackStack<NavKey>(HistoryScreen)
|
||||
|
||||
NavigationController.openChatSessionFromNotification("stored-session")
|
||||
|
||||
assertEquals(ChatScreen, NavigationController.backStack?.lastOrNull())
|
||||
assertNull(NavigationController.consumeChatReturnScreen())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `drawer navigation disarms a stale return screen`() {
|
||||
NavigationController.backStack = NavBackStack<NavKey>(HistoryScreen)
|
||||
NavigationController.openChatSession("stored-session")
|
||||
assertEquals(HistoryScreen, NavigationController.chatReturnScreen)
|
||||
|
||||
// User opens the drawer and picks Skills instead of pressing back.
|
||||
NavigationController.navigateTo(SkillsScreen)
|
||||
|
||||
assertNull("explicit navigation must invalidate the return", NavigationController.consumeChatReturnScreen())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `re-tapping chat in the drawer also disarms the return`() {
|
||||
NavigationController.backStack = NavBackStack<NavKey>(HistoryScreen)
|
||||
NavigationController.openChatSession("stored-session")
|
||||
assertEquals(HistoryScreen, NavigationController.chatReturnScreen)
|
||||
|
||||
// Same-screen dedup would skip the body — the clear must still run.
|
||||
NavigationController.navigateTo(ChatScreen)
|
||||
|
||||
assertNull(NavigationController.consumeChatReturnScreen())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openNewChat does not arm a return screen`() {
|
||||
NavigationController.backStack = NavBackStack<NavKey>(HistoryScreen)
|
||||
|
||||
NavigationController.openNewChat()
|
||||
|
||||
assertNull(NavigationController.consumeChatReturnScreen())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resetTo disarms the return screen`() {
|
||||
NavigationController.backStack = NavBackStack<NavKey>(HistoryScreen)
|
||||
NavigationController.openChatSession("stored-session")
|
||||
assertEquals(HistoryScreen, NavigationController.chatReturnScreen)
|
||||
|
||||
NavigationController.resetTo(ChatScreen)
|
||||
|
||||
assertNull(NavigationController.consumeChatReturnScreen())
|
||||
}
|
||||
|
||||
// ── resetTo: atomic clear + navigate ──────────────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
Reference in new issue
Block a user