feat(ui): add New Chat to History (#1124)
This commit is contained in:
4 files changed
+50
-1
No files matched your search
@@ -12,6 +12,10 @@ data class PendingChatNavigation(
|
|||||||
val requestId: Long,
|
val requestId: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class PendingNewChatNavigation(
|
||||||
|
val requestId: Long,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Central navigation controller with deduplication guard.
|
* Central navigation controller with deduplication guard.
|
||||||
*
|
*
|
||||||
@@ -27,9 +31,12 @@ object NavigationController {
|
|||||||
var backStack: NavBackStack<NavKey>? = null
|
var backStack: NavBackStack<NavKey>? = null
|
||||||
var pendingChatNavigation: PendingChatNavigation? by mutableStateOf(null)
|
var pendingChatNavigation: PendingChatNavigation? by mutableStateOf(null)
|
||||||
private set
|
private set
|
||||||
|
var pendingNewChatNavigation: PendingNewChatNavigation? 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
|
||||||
|
private var nextNewChatNavigationRequestId = 0L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top-level primary screens (all drawer-accessible screens).
|
* Top-level primary screens (all drawer-accessible screens).
|
||||||
@@ -73,6 +80,14 @@ object NavigationController {
|
|||||||
queueChatNavigation(sessionId, scrollToBottom = true)
|
queueChatNavigation(sessionId, scrollToBottom = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openNewChat() {
|
||||||
|
pendingNewChatNavigation =
|
||||||
|
PendingNewChatNavigation(
|
||||||
|
requestId = ++nextNewChatNavigationRequestId,
|
||||||
|
)
|
||||||
|
navigateTo(ChatScreen)
|
||||||
|
}
|
||||||
|
|
||||||
private fun queueChatNavigation(
|
private fun queueChatNavigation(
|
||||||
sessionId: String,
|
sessionId: String,
|
||||||
scrollToBottom: Boolean,
|
scrollToBottom: Boolean,
|
||||||
@@ -90,6 +105,9 @@ object NavigationController {
|
|||||||
fun consumePendingChatNavigation(): PendingChatNavigation? =
|
fun consumePendingChatNavigation(): PendingChatNavigation? =
|
||||||
pendingChatNavigation.also { pendingChatNavigation = null }
|
pendingChatNavigation.also { pendingChatNavigation = null }
|
||||||
|
|
||||||
|
fun consumePendingNewChatNavigation(): PendingNewChatNavigation? =
|
||||||
|
pendingNewChatNavigation.also { pendingNewChatNavigation = null }
|
||||||
|
|
||||||
fun consumePendingSessionId(): String? = consumePendingChatNavigation()?.sessionId
|
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. */
|
||||||
|
|||||||
@@ -45,9 +45,15 @@ fun ChatLifecycleEffects(
|
|||||||
|
|
||||||
// Switch to session from notification/history
|
// Switch to session from notification/history
|
||||||
val pendingNavigation = NavigationController.pendingChatNavigation
|
val pendingNavigation = NavigationController.pendingChatNavigation
|
||||||
LaunchedEffect(sessionId, pendingNavigation, connectionStatus) {
|
val pendingNewChatNavigation = NavigationController.pendingNewChatNavigation
|
||||||
|
LaunchedEffect(sessionId, pendingNavigation, pendingNewChatNavigation, connectionStatus) {
|
||||||
if (connectionStatus != ConnectionStatus.CONNECTED) return@LaunchedEffect
|
if (connectionStatus != ConnectionStatus.CONNECTED) return@LaunchedEffect
|
||||||
|
val newChatRequest = NavigationController.consumePendingNewChatNavigation()
|
||||||
val request = NavigationController.consumePendingChatNavigation()
|
val request = NavigationController.consumePendingChatNavigation()
|
||||||
|
if (newChatRequest != null) {
|
||||||
|
viewModel.createNewSession()
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
val target = request?.sessionId ?: sessionId
|
val target = request?.sessionId ?: sessionId
|
||||||
if (!target.isNullOrBlank()) {
|
if (!target.isNullOrBlank()) {
|
||||||
viewModel.switchSession(target)
|
viewModel.switchSession(target)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.CallSplit
|
import androidx.compose.material.icons.automirrored.filled.CallSplit
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.automirrored.filled.Send
|
import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Code
|
import androidx.compose.material.icons.filled.Code
|
||||||
@@ -466,6 +467,15 @@ fun SessionsScreen(
|
|||||||
isRefreshing = state.isLoading,
|
isRefreshing = state.isLoading,
|
||||||
onRefresh = { viewModel.loadSessions() },
|
onRefresh = { viewModel.loadSessions() },
|
||||||
actions = {
|
actions = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { NavigationController.openNewChat() },
|
||||||
|
modifier = Modifier.testTag("sessions_action_new_chat"),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Add,
|
||||||
|
contentDescription = stringResource(R.string.content_desc_new_chat),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (state.hasHiddenSessions) {
|
if (state.hasHiddenSessions) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { viewModel.toggleShowHidden() },
|
onClick = { viewModel.toggleShowHidden() },
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.navigation3.runtime.NavKey
|
|||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -21,12 +22,14 @@ class NavigationControllerTest {
|
|||||||
// Start fresh — no pinned back stack from a previous test
|
// Start fresh — no pinned back stack from a previous test
|
||||||
NavigationController.backStack = null
|
NavigationController.backStack = null
|
||||||
NavigationController.consumePendingSessionId()
|
NavigationController.consumePendingSessionId()
|
||||||
|
NavigationController.consumePendingNewChatNavigation()
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
NavigationController.backStack = null
|
NavigationController.backStack = null
|
||||||
NavigationController.consumePendingSessionId()
|
NavigationController.consumePendingSessionId()
|
||||||
|
NavigationController.consumePendingNewChatNavigation()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Dedup guard: navigateTo with same key ──────────────────────────────
|
// ── Dedup guard: navigateTo with same key ──────────────────────────────
|
||||||
@@ -119,6 +122,18 @@ class NavigationControllerTest {
|
|||||||
assertNull(NavigationController.consumePendingSessionId())
|
assertNull(NavigationController.consumePendingSessionId())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `openNewChat navigates to chat and exposes a consumable request`() {
|
||||||
|
val backStack = NavBackStack<NavKey>(HistoryScreen)
|
||||||
|
NavigationController.backStack = backStack
|
||||||
|
|
||||||
|
NavigationController.openNewChat()
|
||||||
|
|
||||||
|
assertEquals(ChatScreen, backStack.lastOrNull())
|
||||||
|
assertNotNull(NavigationController.consumePendingNewChatNavigation())
|
||||||
|
assertNull(NavigationController.consumePendingNewChatNavigation())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `openChatSession ignores blank ids`() {
|
fun `openChatSession ignores blank ids`() {
|
||||||
val backStack = NavBackStack<NavKey>(HistoryScreen)
|
val backStack = NavBackStack<NavKey>(HistoryScreen)
|
||||||
|
|||||||
Reference in new issue
Block a user