116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { useAuth } from '@/components/id/auth-provider';
|
|
import { getWsUrl, type ChatMessage } from '@/lib/api';
|
|
|
|
export interface RealtimeEvent {
|
|
userId?: string;
|
|
type: string;
|
|
title: string;
|
|
message: string;
|
|
payload?: Record<string, unknown> & {
|
|
notificationId?: string;
|
|
roomId?: string;
|
|
message?: ChatMessage;
|
|
inviteId?: string;
|
|
groupId?: string;
|
|
};
|
|
}
|
|
|
|
type Listener = (event: RealtimeEvent) => void;
|
|
|
|
interface RealtimeContextValue {
|
|
unreadCount: number;
|
|
setUnreadCount: React.Dispatch<React.SetStateAction<number>>;
|
|
subscribe: (listener: Listener) => () => void;
|
|
connected: boolean;
|
|
}
|
|
|
|
const RealtimeContext = React.createContext<RealtimeContextValue | null>(null);
|
|
|
|
export function RealtimeProvider({ children }: { children: React.ReactNode }) {
|
|
const { user, token, isPinLocked } = useAuth();
|
|
const [unreadCount, setUnreadCount] = React.useState(0);
|
|
const [connected, setConnected] = React.useState(false);
|
|
const listenersRef = React.useRef(new Set<Listener>());
|
|
const socketRef = React.useRef<WebSocket | null>(null);
|
|
const reconnectTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const subscribe = React.useCallback((listener: Listener) => {
|
|
listenersRef.current.add(listener);
|
|
return () => listenersRef.current.delete(listener);
|
|
}, []);
|
|
|
|
const emit = React.useCallback((event: RealtimeEvent) => {
|
|
listenersRef.current.forEach((listener) => listener(event));
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
if (!user || !token || isPinLocked) {
|
|
socketRef.current?.close();
|
|
socketRef.current = null;
|
|
setConnected(false);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
|
|
function connect() {
|
|
if (cancelled) return;
|
|
const url = `${getWsUrl()}?token=${encodeURIComponent(token!)}`;
|
|
const socket = new WebSocket(url);
|
|
socketRef.current = socket;
|
|
|
|
socket.onopen = () => setConnected(true);
|
|
socket.onclose = () => {
|
|
setConnected(false);
|
|
if (!cancelled) {
|
|
reconnectTimer.current = setTimeout(connect, 3000);
|
|
}
|
|
};
|
|
socket.onerror = () => socket.close();
|
|
socket.onmessage = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data as string) as RealtimeEvent;
|
|
emit(data);
|
|
if (
|
|
data.type === 'family_invite' ||
|
|
data.type === 'role_assigned' ||
|
|
data.type === 'role_removed' ||
|
|
(data.type === 'chat_message' && data.payload?.notificationId)
|
|
) {
|
|
setUnreadCount((count) => count + 1);
|
|
}
|
|
} catch {
|
|
// ignore malformed payloads
|
|
}
|
|
};
|
|
}
|
|
|
|
connect();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
|
|
socketRef.current?.close();
|
|
socketRef.current = null;
|
|
setConnected(false);
|
|
};
|
|
}, [emit, isPinLocked, token, user]);
|
|
|
|
return (
|
|
<RealtimeContext.Provider value={{ unreadCount, setUnreadCount, subscribe, connected }}>
|
|
{children}
|
|
</RealtimeContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useRealtime() {
|
|
const context = React.useContext(RealtimeContext);
|
|
if (!context) {
|
|
throw new Error('useRealtime должен использоваться внутри RealtimeProvider');
|
|
}
|
|
return context;
|
|
}
|