fix(chat): 发送后切会话不再误标未读,管理端撤回实时同步到对话页

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 03:54:58 +08:00
parent 1f4e72550a
commit cda5fad2e5
7 changed files with 476 additions and 41 deletions

View File

@@ -18,10 +18,11 @@ import {
apiCreateChatRoom,
apiPinChatRoom,
type ChatRoomView,
type ChatMessage,
type User,
} from "@/lib/api";
import { realtime, RT_CHAT_UNREAD, RT_CHAT_MEMBERSHIP } from "@/lib/realtime";
import type { RtChatUnreadData, RtChatMembershipData } from "@/lib/realtime";
import { realtime, RT_CHAT_UNREAD, RT_CHAT_MEMBERSHIP, RT_CHAT_RECALLED } from "@/lib/realtime";
import type { RtChatUnreadData, RtChatMembershipData, RtChatRecalledData } from "@/lib/realtime";
import { onDocumentVisible } from "@/lib/visibilityReconcile";
import { toast } from "@/lib/toast";
import Avatar from "@/components/Avatar";
@@ -111,13 +112,22 @@ export default function ChatShell({
room: ChatRoomView;
} | null>(null);
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
/** 本人刚发出的消息 ID:拉列表时若水位尚未跟上,避免把这条算成未读 */
const lastSentIdByRoom = useRef<Map<number, number>>(new Map());
/** 拉列表/SSR 回填时,当前打开的会话未读一律置 0(已读请求可能尚未落库) */
const applyRooms = useCallback(
(list: ChatRoomView[]) => {
const next = (list || []).map((r) =>
activeRoomId && r.id === activeRoomId ? { ...r, unread_count: 0 } : r
);
const next = (list || []).map((r) => {
if (activeRoomId && r.id === activeRoomId) {
return { ...r, unread_count: 0 };
}
const sentId = lastSentIdByRoom.current.get(r.id);
if (sentId && (r.last_message_id || 0) <= sentId && (r.unread_count || 0) > 0) {
return { ...r, unread_count: 0 };
}
return r;
});
setRooms(sortRooms(next));
},
[activeRoomId]
@@ -152,9 +162,9 @@ export default function ChatShell({
[applyRooms]
);
// 进入消息壳 / 切会话:HTTP 拉最新排序(避开预取缓存)
// 进入消息壳 / 切会话:HTTP 拉最新排序(避开预取缓存;切房强制拉取以免把刚发出的消息当成未读)
useEffect(() => {
void refreshRooms();
void refreshRooms({ force: true });
}, [refreshRooms, activeRoomId]);
// 从其他标签页切回时对账(10s 限频 + 上方 2s 合并)
@@ -198,6 +208,78 @@ export default function ChatShell({
return off;
}, [activeRoomId, refreshRooms]);
// 本人刚发出的消息:立刻更新侧栏预览并清角标(发送者收不到 chat:unread)
useEffect(() => {
const onSent = (ev: Event) => {
const detail = (ev as CustomEvent<{ roomId?: number; message?: ChatMessage }>).detail;
if (!detail?.roomId || !detail.message) return;
const { roomId, message } = detail;
lastSentIdByRoom.current.set(roomId, message.id);
setRooms((prev) => {
const idx = prev.findIndex((r) => r.id === roomId);
if (idx < 0) return prev;
const next = [...prev];
const cur = next[idx];
next[idx] = {
...cur,
unread_count: 0,
last_message_id: message.id,
last_message: message,
updated_at: message.created_at || cur.updated_at,
};
return sortRooms(next);
});
};
window.addEventListener("j13-chat-sent", onSent);
return () => window.removeEventListener("j13-chat-sent", onSent);
}, []);
// 撤回(含管理端):若撤回的是侧栏预览那条,实时换成撤回文案
useEffect(() => {
const off = realtime.on<RtChatRecalledData>(RT_CHAT_RECALLED, (data) => {
if (!data?.room_id || !data.id) return;
setRooms((prev) => {
const idx = prev.findIndex((r) => r.id === data.room_id);
if (idx < 0) return prev;
const cur = prev[idx];
if (cur.last_message?.id !== data.id && cur.last_message_id !== data.id) {
return prev;
}
const next = [...prev];
next[idx] = {
...cur,
last_message: {
id: data.id,
room_id: data.room_id,
sender_id: data.sender_id || cur.last_message?.sender_id || 0,
content: "",
created_at: cur.last_message?.created_at || new Date().toISOString(),
recalled_at: data.recalled_at || new Date().toISOString(),
recalled_by: data.recalled_by,
sender: cur.last_message?.sender || ({
id: data.sender_id || 0,
username: "",
nickname: "",
avatar: "",
role: "user",
} as User),
recaller: data.recaller
? {
id: data.recaller.id,
username: data.recaller.username,
nickname: data.recaller.nickname,
avatar: data.recaller.avatar,
role: data.recaller.role || "user",
}
: cur.last_message?.recaller,
},
};
return next;
});
});
return off;
}, []);
useEffect(() => {
const off = realtime.on<RtChatMembershipData>(RT_CHAT_MEMBERSHIP, (data) => {
if (!data) return;