feature: version one notification feature implemented.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import type { InboxPage, UserNotificationItem } from '@/types/notifications';
|
||||
import type { LabCaseActivityItem } from '@/types/lab-case-activity';
|
||||
import type { LabCaseTabReadTarget, TabBadgeCounts } from '@/lib/tabBadgeUtils';
|
||||
|
||||
@@ -27,4 +28,29 @@ export const notificationsApi = {
|
||||
const response = await apiClient.post('/notifications/mark-case-read', { labCaseId });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
listInbox: async (params?: {
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}): Promise<{ success: boolean; data: InboxPage }> => {
|
||||
const response = await apiClient.get('/notifications/inbox', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
inboxUnreadCount: async (): Promise<{ success: boolean; data: { count: number } }> => {
|
||||
const response = await apiClient.get('/notifications/inbox/unread-count');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
markInboxRead: async (
|
||||
id: string,
|
||||
): Promise<{ success: boolean; data: UserNotificationItem | null }> => {
|
||||
const response = await apiClient.post(`/notifications/inbox/${id}/read`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
markInboxReadAll: async (): Promise<{ success: boolean }> => {
|
||||
const response = await apiClient.post('/notifications/inbox/read-all');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
100
frontend/src/lib/realtime/RealtimeProvider.tsx
Normal file
100
frontend/src/lib/realtime/RealtimeProvider.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
import { useAuth } from '@/lib/hooks/useAuth';
|
||||
import type { UserNotificationItem } from '@/types/notifications';
|
||||
|
||||
type RealtimeContextValue = {
|
||||
connected: boolean;
|
||||
lastNotification: UserNotificationItem | null;
|
||||
unreadCount: number | null;
|
||||
setUnreadCount: (count: number) => void;
|
||||
};
|
||||
|
||||
const RealtimeContext = createContext<RealtimeContextValue>({
|
||||
connected: false,
|
||||
lastNotification: null,
|
||||
unreadCount: null,
|
||||
setUnreadCount: () => undefined,
|
||||
});
|
||||
|
||||
function apiOrigin(): string {
|
||||
const base = process.env.NEXT_PUBLIC_API_URL ?? '';
|
||||
try {
|
||||
return new URL(base).origin;
|
||||
} catch {
|
||||
return typeof window !== 'undefined' ? window.location.origin : '';
|
||||
}
|
||||
}
|
||||
|
||||
export function RealtimeProvider({ children }: { children: ReactNode }) {
|
||||
const { user, currentOrganization, isAuthReady } = useAuth();
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [lastNotification, setLastNotification] = useState<UserNotificationItem | null>(null);
|
||||
const [unreadCount, setUnreadCount] = useState<number | null>(null);
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthReady || !user || !currentOrganization?.id) {
|
||||
socketRef.current?.disconnect();
|
||||
socketRef.current = null;
|
||||
setConnected(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const socket = io(`${apiOrigin()}/realtime`, {
|
||||
withCredentials: true,
|
||||
transports: ['websocket', 'polling'],
|
||||
});
|
||||
socketRef.current = socket;
|
||||
|
||||
socket.on('connect', () => setConnected(true));
|
||||
socket.on('disconnect', () => setConnected(false));
|
||||
socket.on('notification.created', (payload: { notification?: UserNotificationItem }) => {
|
||||
if (payload?.notification) {
|
||||
setLastNotification(payload.notification);
|
||||
}
|
||||
});
|
||||
socket.on('notification.unreadCount', (payload: { count?: number }) => {
|
||||
if (typeof payload?.count === 'number') {
|
||||
setUnreadCount(payload.count);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.disconnect();
|
||||
socketRef.current = null;
|
||||
setConnected(false);
|
||||
};
|
||||
}, [isAuthReady, user, currentOrganization?.id]);
|
||||
|
||||
const setUnreadCountStable = useCallback((count: number) => {
|
||||
setUnreadCount(count);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
connected,
|
||||
lastNotification,
|
||||
unreadCount,
|
||||
setUnreadCount: setUnreadCountStable,
|
||||
}),
|
||||
[connected, lastNotification, unreadCount, setUnreadCountStable],
|
||||
);
|
||||
|
||||
return <RealtimeContext.Provider value={value}>{children}</RealtimeContext.Provider>;
|
||||
}
|
||||
|
||||
export function useRealtime() {
|
||||
return useContext(RealtimeContext);
|
||||
}
|
||||
Reference in New Issue
Block a user