feat(web): D4 会话未读徽标 — 打开即已读 + 消费 session_read
- SessionMeta.unreadCount(可选,兼容旧后端)+ api/sessions.markSessionRead - lib/sessionRead.ts:会话历史落地后再推游标(best-effort,失败静默,下次 GET /sessions 收敛)+ 帧归属判定(ws.ts 因此留在 500 行内) - store.clearUnread 无变化时返回同一 state,避免多余重渲染;Nav 徽标沿用 Miuix token(mu-radius-pill/mu-err/mu-rise-in) - 逻辑测试 35 → 54 项,含变异校验(改成 clearUnread(activeSessionId) 即 FAIL)
This commit is contained in:
1 parent
e8eccd2bb2
commit
a828edacc5
9 files changed
+235
-11
No files matched your search
@@ -6,6 +6,7 @@ import { AdminApp } from "./admin/AdminApp";
|
||||
import { Login } from "./components/Login";
|
||||
import { parseAdminHash } from "./admin/router";
|
||||
import { connectWs, disconnectWs } from "./lib/ws";
|
||||
import { markSessionOpenedRead } from "./lib/sessionRead";
|
||||
import { initTheme } from "./lib/theme";
|
||||
import {
|
||||
fetchMessages,
|
||||
@@ -100,13 +101,21 @@ export default function App(): React.ReactNode {
|
||||
}, [token, setSessions, setActiveSession]);
|
||||
|
||||
// Whenever the focused session changes (switch, new, or first load) and it is
|
||||
// a session we know about, pull its initial page of history into the panel.
|
||||
// a session we know about, pull its initial page of history into the panel, then
|
||||
// advance that session's read cursor (协同 B3/D4) — *after* the page lands, so the
|
||||
// cursor never passes messages the user was never shown. The effect hinges on
|
||||
// *whether* the session is known rather than on the list's identity: clearing the
|
||||
// unread badge rewrites `sessions`, and keying on it would re-fetch history (and
|
||||
// so double every session switch) for a metadata-only change.
|
||||
const activeSessionKnown = Boolean(sessions[activeSessionId]);
|
||||
useEffect(() => {
|
||||
if (!activeSessionId || !sessions[activeSessionId]) return;
|
||||
if (!activeSessionId || !activeSessionKnown) return;
|
||||
let cancelled = false;
|
||||
fetchMessages(activeSessionId, undefined, 50)
|
||||
.then((list) => {
|
||||
if (!cancelled) setMessages(list);
|
||||
if (cancelled) return;
|
||||
setMessages(list);
|
||||
markSessionOpenedRead(activeSessionId);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setMessages([]);
|
||||
@@ -114,7 +123,7 @@ export default function App(): React.ReactNode {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activeSessionId, sessions, setMessages]);
|
||||
}, [activeSessionId, activeSessionKnown, setMessages]);
|
||||
|
||||
// The M9 modal UI and the admin subsystem (design/10) are two separate shells.
|
||||
// `/admin` is reached via `#/admin`; every admin module is a hash segment.
|
||||
|
||||
Reference in new issue
Block a user