diff --git a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatHomeScreen.kt b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatHomeScreen.kt index 04d874d..a91bdd2 100644 --- a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatHomeScreen.kt +++ b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatHomeScreen.kt @@ -2,6 +2,7 @@ package top.yeij.cyrene.ui.chat import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -10,6 +11,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory +import kotlinx.coroutines.delay import top.yeij.cyrene.app.AppContainer import top.yeij.cyrene.ui.admin.AdminHomeScreen import top.yeij.cyrene.ui.common.MiuixDrawer @@ -18,6 +20,19 @@ import top.yeij.cyrene.ui.sessions.SessionsViewModel import top.yeij.cyrene.ui.settings.AboutScreen import top.yeij.cyrene.ui.settings.SettingsScreen +/** 「这个会话在服务端已经没了」那句告知挂多久(毫秒):它描述一次刚发生的动作,不是持续状态。 */ +private const val SESSION_GONE_NOTICE_MILLIS = 8_000L + +/** + * 会话在别处被删掉时那句如实告知。 + * + * 它必须由**这一层**持有,不能挂在 `ChatViewModel` 上:`onSessionGone` 一被调用, + * `activeSessionId` 就会被收敛回主会话,那个会话的 VM 连同它的状态**立刻被销毁**, + * 挂在上面的横幅一个字都来不及显示(真机实测过)。这一层的 `remember` 活得比它久。 + */ +private const val SESSION_GONE_MESSAGE = "这个会话在服务端已经不存在了(可能已在别处删除),已切回主会话" + + /** * 聊天页 + 侧边栏(会话切换)+ 设置页 + 管理员监控。 * 返回键逐层回退:管理/设置 → 聊天;抽屉打开时先关抽屉;都关着才交给系统(回桌面)。 @@ -36,6 +51,17 @@ fun ChatHomeScreen( var showAdmin by remember { mutableStateOf(false) } var showAbout by remember { mutableStateOf(false) } + // 当前会话在别处被删掉时那句告知(见 [SESSION_GONE_MESSAGE])。放在这一层是因为承载它的 + // 会话 VM 会在切回主会话的瞬间被销毁。 + var sessionNotice by remember { mutableStateOf(null) } + + LaunchedEffect(sessionNotice) { + if (sessionNotice != null) { + delay(SESSION_GONE_NOTICE_MILLIS) + sessionNotice = null + } + } + BackHandler(enabled = showAdmin) { showAdmin = false } // 「关于」嵌在设置页里,所以两个条件必须**互斥**:在关于页时设置页的处理器关掉, // 返回才只退一层(关于 → 设置),不会一步跳回聊天。 @@ -75,10 +101,17 @@ fun ChatHomeScreen( sessionsViewModel.refresh(silent = true) }, onLogout = onLogout, - // 当前会话在别处被删了(`404 session_not_found`)⇒ 重拉列表:`refresh` 里面会把 - // 「指向已删会话」的 activeSessionId 收敛回主会话(`resolveActiveSession`), - // 抽屉里那条也跟着消失。所以这里不需要自己判「该切到哪儿」。 - onSessionGone = { sessionsViewModel.refresh(silent = true) }, + // 当前会话在别处被删了(`404 session_not_found`)⇒ 先立起那句如实告知,再重拉列表: + // `refresh` 里面会把「指向已删会话」的 activeSessionId 收敛回主会话 + // (`resolveActiveSession`),抽屉里那条也跟着消失。所以这里不需要自己判「该切到哪儿」。 + // + // 顺序很重要:告知必须**先**立起来再刷新——刷新会让本层的 ChatScreen 换 key、 + // 把那个会话的 VM 销毁掉,晚一步就什么都看不到了。 + onSessionGone = { + sessionNotice = SESSION_GONE_MESSAGE + sessionsViewModel.refresh(silent = true) + }, + sessionNotice = sessionNotice, ) }, drawerContent = { diff --git a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt index f034475..50c232e 100644 --- a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt +++ b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt @@ -140,6 +140,7 @@ fun ChatScreen( onOpenDrawer: () -> Unit = {}, onLogout: () -> Unit = {}, onSessionGone: () -> Unit = {}, + sessionNotice: String? = null, ) { val viewModel: ChatViewModel = viewModel( key = sessionId, @@ -151,6 +152,11 @@ fun ChatScreen( // 这个会话在服务端已经没了(`404 session_not_found`,契约 v0.63):交给上层把用户带回主会话。 // 只有**非主会话**会置这个位(主会话回 404 说明部署出了问题,那时该显示的是普通错误横幅), // 所以这里不会出现「已经主会话还往上跳一次」的回环。 + // + // ⚠️ **那句解释不能挂在这个 VM 上**:`onSessionGone` 一被调用,上层就会把 activeSessionId 收敛回 + // 主会话,`key = sessionId` 随之变化 ⇒ **这个 VM 连同它的状态一起被销毁**,挂在上面的横幅 + // 一个字都来不及显示(真机实测过:切回主会话干净利落,用户完全不知道为什么)。 + // 所以文案由 [sessionNotice] 从上层注入(那个状态活得比本 VM 久)。 LaunchedEffect(state.sessionGone) { if (state.sessionGone) onSessionGone() } @@ -493,11 +499,10 @@ fun ChatScreen( state.error?.let { message -> Banner(text = message, color = MiuixTheme.colorScheme.error) } - if (state.sessionGone) { - Banner( - text = "这个会话在服务端已经不存在了(可能已在别处删除),正在切回主会话", - color = MiuixTheme.colorScheme.error, - ) + // 「这个会话在服务端已经没了」那句解释:由上层注入(见函数头上那段说明), + // 因为承载它的会话 VM 在切走的那一刻就被销毁了。 + sessionNotice?.let { message -> + Banner(text = message, color = MiuixTheme.colorScheme.error) } state.shareNotice?.let { message -> Banner(text = message, color = MiuixTheme.colorScheme.onSurfaceVariantSummary)