diff --git a/frontend/src/hooks/usePresence.js b/frontend/src/hooks/usePresence.js new file mode 100644 index 0000000..f1c22d5 --- /dev/null +++ b/frontend/src/hooks/usePresence.js @@ -0,0 +1,83 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; + +const WS_RECONNECT_DELAY = 3000; +const PING_INTERVAL = 30000; + +export function usePresence() { + const { token, adminName, isAuthenticated } = useAuth(); + const location = useLocation(); + const [viewers, setViewers] = useState([]); + const wsRef = useRef(null); + const pingRef = useRef(null); + const reconnectRef = useRef(null); + + const getWsUrl = useCallback(() => { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${protocol}//${window.location.host}/api/sessions/live`; + }, []); + + const connect = useCallback(() => { + if (!isAuthenticated || !token) return; + + const ws = new WebSocket(getWsUrl()); + wsRef.current = ws; + + ws.onopen = () => { + ws.send(JSON.stringify({ type: 'auth', token })); + }; + + ws.onmessage = (event) => { + const msg = JSON.parse(event.data); + + if (msg.type === 'auth_success') { + ws.send(JSON.stringify({ type: 'page_focus', page: location.pathname })); + + pingRef.current = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'ping' })); + } + }, PING_INTERVAL); + } + + if (msg.type === 'presence_update') { + const currentPage = location.pathname; + const onSamePage = msg.admins + .filter(a => a.page === currentPage) + .map(a => a.name === adminName ? 'me' : a.name); + setViewers(onSamePage); + } + }; + + ws.onclose = () => { + clearInterval(pingRef.current); + reconnectRef.current = setTimeout(connect, WS_RECONNECT_DELAY); + }; + + ws.onerror = () => { + ws.close(); + }; + }, [isAuthenticated, token, adminName, location.pathname, getWsUrl]); + + useEffect(() => { + connect(); + + return () => { + clearTimeout(reconnectRef.current); + clearInterval(pingRef.current); + if (wsRef.current) { + wsRef.current.onclose = null; + wsRef.current.close(); + } + }; + }, [connect]); + + useEffect(() => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify({ type: 'page_focus', page: location.pathname })); + } + }, [location.pathname]); + + return { viewers }; +}