This commit is contained in:
@@ -1,18 +1,59 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { type ChatMessage, getWsUrl } from '../api';
|
||||
import { useAuth } from '../auth/AuthContext';
|
||||
import { playChatSound } from '../utils/notifySound';
|
||||
import { playChatSound, playAlertSound } from '../utils/notifySound';
|
||||
|
||||
type ChatIncomingHandler = (msg: ChatMessage) => void;
|
||||
export type PresenceUpdate = { studentId: number; online: boolean; classId?: number };
|
||||
export type StudentViolationEvent = {
|
||||
studentId: number;
|
||||
studentName?: string;
|
||||
studentCode?: string;
|
||||
kind: string;
|
||||
reason: string;
|
||||
monitorMode?: string;
|
||||
classId?: number;
|
||||
};
|
||||
|
||||
const handlers = new Set<ChatIncomingHandler>();
|
||||
const chatHandlers = new Set<ChatIncomingHandler>();
|
||||
const presenceHandlers = new Set<(p: PresenceUpdate) => void>();
|
||||
const violationHandlers = new Set<(v: StudentViolationEvent) => void>();
|
||||
|
||||
export function onStaffChatMessage(handler: ChatIncomingHandler): () => void {
|
||||
handlers.add(handler);
|
||||
return () => { handlers.delete(handler); };
|
||||
chatHandlers.add(handler);
|
||||
return () => { chatHandlers.delete(handler); };
|
||||
}
|
||||
|
||||
/** WebSocket luôn bật khi đã login — nhận tin sinh viên realtime */
|
||||
export function onStudentPresence(handler: (p: PresenceUpdate) => void): () => void {
|
||||
presenceHandlers.add(handler);
|
||||
return () => { presenceHandlers.delete(handler); };
|
||||
}
|
||||
|
||||
export function onStudentViolation(handler: (v: StudentViolationEvent) => void): () => void {
|
||||
violationHandlers.add(handler);
|
||||
return () => { violationHandlers.delete(handler); };
|
||||
}
|
||||
|
||||
function scheduleBackoff(attempt: number): number {
|
||||
const base = Math.min(1000 * 2 ** Math.min(attempt, 4), 10000);
|
||||
return base + Math.floor(Math.random() * 250);
|
||||
}
|
||||
|
||||
export function kindLabel(kind: string): string {
|
||||
switch (kind) {
|
||||
case 'app_closed': return 'Tự đóng app';
|
||||
case 'unclean_shutdown': return 'Tắt đột ngột';
|
||||
case 'multi_monitor': return 'Nhiều màn hình';
|
||||
case 'user_switch': return 'Đổi user';
|
||||
case 'session_change': return 'Khóa / đổi phiên';
|
||||
case 'virtual_desktop': return 'Desktop ảo';
|
||||
case 'wifi': return 'WiFi trái phép';
|
||||
case 'guard': return 'Vi phạm môi trường';
|
||||
default: return kind || 'Vi phạm';
|
||||
}
|
||||
}
|
||||
|
||||
/** WebSocket luôn bật khi đã login — chat + presence + violation + keepalive. */
|
||||
export function StaffChatSocket() {
|
||||
const { staff, token } = useAuth();
|
||||
const staffIdRef = useRef(0);
|
||||
@@ -21,8 +62,8 @@ export function StaffChatSocket() {
|
||||
staffIdRef.current = staff?.id ?? 0;
|
||||
}, [staff?.id]);
|
||||
|
||||
const dispatch = useCallback((msg: ChatMessage) => {
|
||||
handlers.forEach((h) => h(msg));
|
||||
const dispatchChat = useCallback((msg: ChatMessage) => {
|
||||
chatHandlers.forEach((h) => h(msg));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -31,26 +72,74 @@ export function StaffChatSocket() {
|
||||
const wsUrl = getWsUrl(`/ws?role=teacher&staffId=${staff.id}`);
|
||||
let ws: WebSocket | null = null;
|
||||
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let pingTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let closed = false;
|
||||
let attempt = 0;
|
||||
|
||||
const clearPing = () => {
|
||||
if (pingTimer) {
|
||||
clearInterval(pingTimer);
|
||||
pingTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const connect = () => {
|
||||
if (closed) return;
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
attempt = 0;
|
||||
clearPing();
|
||||
pingTimer = setInterval(() => {
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ event: 'client:ping', data: {} }));
|
||||
}
|
||||
}, 15000);
|
||||
};
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
const payload = JSON.parse(ev.data);
|
||||
if (payload.event === 'client:pong') return;
|
||||
if (payload.event === 'presence:update') {
|
||||
const studentId = Number(payload.data?.studentId ?? 0);
|
||||
if (!studentId) return;
|
||||
presenceHandlers.forEach((h) => h({
|
||||
studentId,
|
||||
online: !!payload.data?.online,
|
||||
classId: Number(payload.data?.classId ?? 0) || undefined,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (payload.event === 'teacher:student-violation') {
|
||||
const studentId = Number(payload.data?.studentId ?? 0);
|
||||
if (!studentId) return;
|
||||
playAlertSound();
|
||||
violationHandlers.forEach((h) => h({
|
||||
studentId,
|
||||
studentName: payload.data?.studentName || '',
|
||||
studentCode: payload.data?.studentCode || '',
|
||||
kind: String(payload.data?.kind || ''),
|
||||
reason: String(payload.data?.reason || ''),
|
||||
monitorMode: payload.data?.monitorMode || '',
|
||||
classId: Number(payload.data?.classId ?? 0) || undefined,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (payload.event !== 'chat:message') return;
|
||||
const msg = payload.data as ChatMessage;
|
||||
const targetStaff = Number(msg.targetStaffId ?? msg.staffId ?? 0);
|
||||
if (targetStaff > 0 && targetStaff !== staffIdRef.current) return;
|
||||
dispatch(msg);
|
||||
dispatchChat(msg);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
clearPing();
|
||||
if (!closed) {
|
||||
retryTimer = setTimeout(connect, 3000);
|
||||
retryTimer = setTimeout(connect, scheduleBackoff(attempt++));
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -59,12 +148,13 @@ export function StaffChatSocket() {
|
||||
|
||||
return () => {
|
||||
closed = true;
|
||||
clearPing();
|
||||
if (retryTimer) clearTimeout(retryTimer);
|
||||
ws?.close();
|
||||
};
|
||||
}, [token, staff?.id, dispatch]);
|
||||
}, [token, staff?.id, dispatchChat]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export { playChatSound };
|
||||
export { playChatSound, playAlertSound };
|
||||
|
||||
Reference in New Issue
Block a user