fix(chat): 「会话已删」那句解释必须由不会被销毁的那一层持有
真机验收 404 兜底时发现的真问题:**切换本身是对的,但用户看不到任何解释**。
- logcat 链路完整:`GET /sessions/web:{已删}/messages?limit=50` → `404` →(约 0.9 秒后)
`GET /sessions` → 界面切回主会话。也就是说「退回主会话 + 如实说一句」里的**前半句成了**。
- 但**后半句一个字都没显示**:那句横幅原来挂在 `ChatUiState.sessionGone` 上,而
`onSessionGone` 一被调用,`activeSessionId` 就被收敛回主会话、`ChatScreen` 的 `key` 随之变化,
承载它的那个 `ChatViewModel` 连同状态**立刻被销毁** —— 横幅来不及组合出来就没了。
(这正是「把一次性告知放在会被销毁的状态里」的经典坑,值得单记一条。)
改法:
- `ChatUiState.sessionGone` 保留,但只当**触发信号**(KDoc 已改写说明这一点);
- 文案由 `ChatHomeScreen` 的 `remember` 持有(它活得比会话 VM 久),通过新的
`sessionNotice: String?` 参数注入 `ChatScreen`,仍然走页面上那同一排 `Banner`,
8 秒后自己消失(它描述一次刚发生的动作,不是持续状态);
- `onSessionGone` 里**先**立起告知、**再**刷新列表——顺序反了同样什么都看不到。
验证:装机复测通过,红字横幅如期出现在主会话页顶部
(证据 `app/build/e2e-shots/w7-gone-2.png`),logcat 链路 404 → `GET /sessions` 完整。
assembleDebug 与 testDebugUnitTest 全绿(884 测试 / 0 失败,与改动前同数:本次只动了接线与文案,
没有可新增的纯函数)。
This commit is contained in:
1 parent
1794e53d1c
commit
b5c0b13210
2 files changed
+47
-9
No files matched your search
@@ -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<String?>(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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user