first commit
This commit is contained in:
110
apps/frontend/components/notifications/realtime-provider.tsx
Normal file
110
apps/frontend/components/notifications/realtime-provider.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import { WS_URL, 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 = `${WS_URL}?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 === '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;
|
||||
}
|
||||
Reference in New Issue
Block a user