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
+137
-3
No files matched your search
@@ -215,8 +215,15 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
var lastBackPressTime by remember { mutableLongStateOf(0L) }
|
var lastBackPressTime by remember { mutableLongStateOf(0L) }
|
||||||
val backToastText = stringResource(R.string.press_back_again_to_exit)
|
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) {
|
BackHandler(enabled = currentScreen == ChatScreen && backStack.size == 1) {
|
||||||
|
val pendingReturn = NavigationController.chatReturnScreen
|
||||||
|
if (pendingReturn != null) {
|
||||||
|
NavigationController.consumeChatReturnScreen()
|
||||||
|
NavigationController.navigateTo(pendingReturn)
|
||||||
|
} else {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
if (now - lastBackPressTime < 2000L) {
|
if (now - lastBackPressTime < 2000L) {
|
||||||
(context as? Activity)?.finish()
|
(context as? Activity)?.finish()
|
||||||
@@ -225,6 +232,7 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
Toast.makeText(context, backToastText, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, backToastText, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Safety fallback: if a non-chat screen somehow sits alone on stack, swipe back returns to ChatScreen
|
// Safety fallback: if a non-chat screen somehow sits alone on stack, swipe back returns to ChatScreen
|
||||||
BackHandler(
|
BackHandler(
|
||||||
|
|||||||
@@ -33,6 +33,16 @@ object NavigationController {
|
|||||||
private set
|
private set
|
||||||
var pendingNewChatNavigation: PendingNewChatNavigation? by mutableStateOf(null)
|
var pendingNewChatNavigation: PendingNewChatNavigation? by mutableStateOf(null)
|
||||||
private set
|
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
|
val pendingSessionId: String? get() = pendingChatNavigation?.sessionId
|
||||||
|
|
||||||
private var nextChatNavigationRequestId = 0L
|
private var nextChatNavigationRequestId = 0L
|
||||||
@@ -45,8 +55,18 @@ object NavigationController {
|
|||||||
*/
|
*/
|
||||||
fun isPrimaryScreen(key: NavKey): Boolean = key == ChatScreen || ScreenRegistry.ALL_SCREENS.any { it.key == key }
|
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) {
|
fun navigateTo(key: NavKey) {
|
||||||
val stack = backStack ?: return
|
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 (stack.lastOrNull() == key) return
|
||||||
|
|
||||||
if (key == ChatScreen) {
|
if (key == ChatScreen) {
|
||||||
@@ -73,11 +93,16 @@ object NavigationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun openChatSession(sessionId: String) {
|
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) {
|
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() {
|
fun openNewChat() {
|
||||||
@@ -91,8 +116,18 @@ object NavigationController {
|
|||||||
private fun queueChatNavigation(
|
private fun queueChatNavigation(
|
||||||
sessionId: String,
|
sessionId: String,
|
||||||
scrollToBottom: Boolean,
|
scrollToBottom: Boolean,
|
||||||
|
returnToList: Boolean,
|
||||||
) {
|
) {
|
||||||
if (sessionId.isBlank()) return
|
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 =
|
||||||
PendingChatNavigation(
|
PendingChatNavigation(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
@@ -100,6 +135,8 @@ object NavigationController {
|
|||||||
requestId = ++nextChatNavigationRequestId,
|
requestId = ++nextChatNavigationRequestId,
|
||||||
)
|
)
|
||||||
navigateTo(ChatScreen)
|
navigateTo(ChatScreen)
|
||||||
|
// Set after navigateTo so the reset itself does not disarm the return.
|
||||||
|
chatReturnScreen = returnScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consumePendingChatNavigation(): PendingChatNavigation? =
|
fun consumePendingChatNavigation(): PendingChatNavigation? =
|
||||||
@@ -113,6 +150,7 @@ object NavigationController {
|
|||||||
/** Clear the stack and navigate to the given screen atomically. */
|
/** Clear the stack and navigate to the given screen atomically. */
|
||||||
fun resetTo(screen: NavKey) {
|
fun resetTo(screen: NavKey) {
|
||||||
val stack = backStack ?: return
|
val stack = backStack ?: return
|
||||||
|
chatReturnScreen = null
|
||||||
stack.clear()
|
stack.clear()
|
||||||
stack.add(screen)
|
stack.add(screen)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class NavigationControllerTest {
|
|||||||
NavigationController.backStack = null
|
NavigationController.backStack = null
|
||||||
NavigationController.consumePendingSessionId()
|
NavigationController.consumePendingSessionId()
|
||||||
NavigationController.consumePendingNewChatNavigation()
|
NavigationController.consumePendingNewChatNavigation()
|
||||||
|
NavigationController.consumeChatReturnScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -30,6 +31,7 @@ class NavigationControllerTest {
|
|||||||
NavigationController.backStack = null
|
NavigationController.backStack = null
|
||||||
NavigationController.consumePendingSessionId()
|
NavigationController.consumePendingSessionId()
|
||||||
NavigationController.consumePendingNewChatNavigation()
|
NavigationController.consumePendingNewChatNavigation()
|
||||||
|
NavigationController.consumeChatReturnScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Dedup guard: navigateTo with same key ──────────────────────────────
|
// ── Dedup guard: navigateTo with same key ──────────────────────────────
|
||||||
@@ -171,6 +173,92 @@ class NavigationControllerTest {
|
|||||||
assertFalse(NavigationController.consumePendingChatNavigation()?.scrollToBottom == true)
|
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 ──────────────────────────────────
|
// ── resetTo: atomic clear + navigate ──────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in new issue
Block a user