fix(notification): open tapped chat at bottom (#984)
Co-authored-by: M57 <41645758+Hy4ri@users.noreply.github.com>
This commit is contained in:
@@ -96,6 +96,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
if (intent?.action != ACTION_OPEN_CHAT_FROM_NOTIFICATION) return
|
if (intent?.action != ACTION_OPEN_CHAT_FROM_NOTIFICATION) return
|
||||||
val sessionId = intent.getStringExtra(NotificationReplyReceiver.EXTRA_SESSION_ID)
|
val sessionId = intent.getStringExtra(NotificationReplyReceiver.EXTRA_SESSION_ID)
|
||||||
intent.removeExtra(NotificationReplyReceiver.EXTRA_SESSION_ID)
|
intent.removeExtra(NotificationReplyReceiver.EXTRA_SESSION_ID)
|
||||||
sessionId?.takeIf { it.isNotBlank() }?.let(NavigationController::openChatSession)
|
sessionId?.takeIf { it.isNotBlank() }?.let(NavigationController::openChatSessionFromNotification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.navigation3.runtime.NavBackStack
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
|
||||||
|
data class PendingChatNavigation(
|
||||||
|
val sessionId: String,
|
||||||
|
val scrollToBottom: Boolean,
|
||||||
|
val requestId: Long,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Central navigation controller with deduplication guard.
|
* Central navigation controller with deduplication guard.
|
||||||
*
|
*
|
||||||
@@ -19,8 +25,11 @@ import androidx.navigation3.runtime.NavKey
|
|||||||
*/
|
*/
|
||||||
object NavigationController {
|
object NavigationController {
|
||||||
var backStack: NavBackStack<NavKey>? = null
|
var backStack: NavBackStack<NavKey>? = null
|
||||||
var pendingSessionId: String? by mutableStateOf(null)
|
var pendingChatNavigation: PendingChatNavigation? by mutableStateOf(null)
|
||||||
private set
|
private set
|
||||||
|
val pendingSessionId: String? get() = pendingChatNavigation?.sessionId
|
||||||
|
|
||||||
|
private var nextChatNavigationRequestId = 0L
|
||||||
|
|
||||||
// Top-level primary screens (Chat, Skills, Cron, System, Settings)
|
// Top-level primary screens (Chat, Skills, Cron, System, Settings)
|
||||||
private val primaryScreens: MutableSet<NavKey> =
|
private val primaryScreens: MutableSet<NavKey> =
|
||||||
@@ -50,12 +59,31 @@ object NavigationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun openChatSession(sessionId: String) {
|
fun openChatSession(sessionId: String) {
|
||||||
|
queueChatNavigation(sessionId, scrollToBottom = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openChatSessionFromNotification(sessionId: String) {
|
||||||
|
queueChatNavigation(sessionId, scrollToBottom = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queueChatNavigation(
|
||||||
|
sessionId: String,
|
||||||
|
scrollToBottom: Boolean,
|
||||||
|
) {
|
||||||
if (sessionId.isBlank()) return
|
if (sessionId.isBlank()) return
|
||||||
pendingSessionId = sessionId
|
pendingChatNavigation =
|
||||||
|
PendingChatNavigation(
|
||||||
|
sessionId = sessionId,
|
||||||
|
scrollToBottom = scrollToBottom,
|
||||||
|
requestId = ++nextChatNavigationRequestId,
|
||||||
|
)
|
||||||
navigateTo(ChatScreen)
|
navigateTo(ChatScreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consumePendingSessionId(): String? = pendingSessionId.also { pendingSessionId = null }
|
fun consumePendingChatNavigation(): PendingChatNavigation? =
|
||||||
|
pendingChatNavigation.also { pendingChatNavigation = null }
|
||||||
|
|
||||||
|
fun consumePendingSessionId(): String? = consumePendingChatNavigation()?.sessionId
|
||||||
|
|
||||||
/** 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) {
|
||||||
|
|||||||
@@ -44,13 +44,17 @@ fun ChatLifecycleEffects(
|
|||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
// Switch to session from notification/history
|
// Switch to session from notification/history
|
||||||
val pendingSessionId = NavigationController.pendingSessionId
|
val pendingNavigation = NavigationController.pendingChatNavigation
|
||||||
LaunchedEffect(sessionId, pendingSessionId, connectionStatus) {
|
LaunchedEffect(sessionId, pendingNavigation, connectionStatus) {
|
||||||
if (connectionStatus != ConnectionStatus.CONNECTED) return@LaunchedEffect
|
if (connectionStatus != ConnectionStatus.CONNECTED) return@LaunchedEffect
|
||||||
val target = NavigationController.consumePendingSessionId() ?: sessionId
|
val request = NavigationController.consumePendingChatNavigation()
|
||||||
|
val target = request?.sessionId ?: sessionId
|
||||||
if (!target.isNullOrBlank()) {
|
if (!target.isNullOrBlank()) {
|
||||||
viewModel.switchSession(target)
|
viewModel.switchSession(target)
|
||||||
}
|
}
|
||||||
|
if (request?.scrollToBottom == true) {
|
||||||
|
scrollController.jumpToBottom(animated = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Land instantly at the bottom on a session switch (issue #682).
|
// Land instantly at the bottom on a session switch (issue #682).
|
||||||
|
|||||||
@@ -146,6 +146,32 @@ class NavigationControllerTest {
|
|||||||
assertNull(NavigationController.consumePendingSessionId())
|
assertNull(NavigationController.consumePendingSessionId())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `notification chat request asks for bottom even when chat is already active`() {
|
||||||
|
val backStack = NavBackStack<NavKey>(ChatScreen)
|
||||||
|
NavigationController.backStack = backStack
|
||||||
|
|
||||||
|
NavigationController.openChatSessionFromNotification("stored-session")
|
||||||
|
val first = NavigationController.consumePendingChatNavigation()
|
||||||
|
NavigationController.openChatSessionFromNotification("stored-session")
|
||||||
|
val second = NavigationController.consumePendingChatNavigation()
|
||||||
|
|
||||||
|
assertEquals(1, backStack.size)
|
||||||
|
assertEquals("stored-session", first?.sessionId)
|
||||||
|
assertTrue(first?.scrollToBottom == true)
|
||||||
|
assertTrue(second?.scrollToBottom == true)
|
||||||
|
assertTrue("repeated taps must be distinct events", first?.requestId != second?.requestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `history chat request does not force bottom`() {
|
||||||
|
NavigationController.backStack = NavBackStack<NavKey>(HistoryScreen)
|
||||||
|
|
||||||
|
NavigationController.openChatSession("stored-session")
|
||||||
|
|
||||||
|
assertFalse(NavigationController.consumePendingChatNavigation()?.scrollToBottom == true)
|
||||||
|
}
|
||||||
|
|
||||||
// ── resetTo: atomic clear + navigate ──────────────────────────────────
|
// ── resetTo: atomic clear + navigate ──────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user