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