112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
export interface UserStatus {
|
|
user_id: number;
|
|
is_online: boolean;
|
|
last_activity: string | null;
|
|
}
|
|
|
|
// Глобальное хранилище статусов
|
|
const userStatuses = new Map<number, UserStatus>();
|
|
const listeners = new Set<(s: UserStatus) => void>();
|
|
|
|
export const subscribeToUserStatus = (cb: (s: UserStatus) => void) => {
|
|
listeners.add(cb);
|
|
return () => listeners.delete(cb);
|
|
};
|
|
|
|
export const getUserStatus = (userId: number): UserStatus | null => userStatuses.get(userId) || null;
|
|
|
|
const updateUserStatus = (s: UserStatus) => {
|
|
userStatuses.set(s.user_id, s);
|
|
listeners.forEach((cb) => {
|
|
try {
|
|
cb(s);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
});
|
|
};
|
|
|
|
export const usePresenceWebSocket = (options?: { enabled?: boolean }) => {
|
|
const enabled = options?.enabled ?? true;
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
const wsRef = useRef<WebSocket | null>(null);
|
|
const pingRef = useRef<number | null>(null);
|
|
|
|
const connect = useCallback(() => {
|
|
if (!enabled) return;
|
|
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
|
|
if (!token) return;
|
|
|
|
let apiUrl = process.env.NEXT_PUBLIC_API_URL || window.location.origin;
|
|
apiUrl = apiUrl.replace(/\/api\/?$/, '').replace(/\/api\//, '/');
|
|
apiUrl = apiUrl.replace(/\/$/, '');
|
|
|
|
const wsProtocol = apiUrl.startsWith('https') ? 'wss:' : 'ws:';
|
|
const wsHost = apiUrl.replace(/^https?:\/\//, '');
|
|
const wsUrl = `${wsProtocol}//${wsHost}/ws/presence/?token=${token}`;
|
|
|
|
const ws = new WebSocket(wsUrl);
|
|
wsRef.current = ws;
|
|
|
|
ws.onopen = () => {
|
|
setIsConnected(true);
|
|
// ping каждые 30 секунд
|
|
pingRef.current = window.setInterval(() => {
|
|
try {
|
|
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'ping' }));
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}, 30000);
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
setIsConnected(false);
|
|
if (pingRef.current) window.clearInterval(pingRef.current);
|
|
pingRef.current = null;
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
setIsConnected(false);
|
|
};
|
|
|
|
ws.onmessage = (ev) => {
|
|
try {
|
|
const data = JSON.parse(ev.data);
|
|
if (data?.type === 'user_status_update' && typeof data.user_id === 'number') {
|
|
updateUserStatus({
|
|
user_id: data.user_id,
|
|
is_online: !!data.is_online,
|
|
last_activity: data.last_activity ?? null,
|
|
});
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
};
|
|
}, [enabled]);
|
|
|
|
const disconnect = useCallback(() => {
|
|
if (pingRef.current) window.clearInterval(pingRef.current);
|
|
pingRef.current = null;
|
|
if (wsRef.current) {
|
|
wsRef.current.close();
|
|
wsRef.current = null;
|
|
}
|
|
setIsConnected(false);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
disconnect();
|
|
connect();
|
|
return () => disconnect();
|
|
}, [connect, disconnect]);
|
|
|
|
return { isConnected };
|
|
};
|
|
|