All checks were successful
Build & Push Docker Image / build (push) Successful in 6m27s
95 lines
2.9 KiB
JavaScript
95 lines
2.9 KiB
JavaScript
import { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
|
||
import toast from 'react-hot-toast';
|
||
import { useAuth } from './AuthContext';
|
||
import api from '../services/api';
|
||
|
||
const NotificationContext = createContext();
|
||
|
||
export function NotificationProvider({ children }) {
|
||
const { user } = useAuth();
|
||
const [notifications, setNotifications] = useState([]);
|
||
const [unreadCount, setUnreadCount] = useState(0);
|
||
// Track seen IDs to detect genuinely new arrivals and show toasts
|
||
const seenIds = useRef(new Set());
|
||
const initialized = useRef(false);
|
||
|
||
const fetch = useCallback(async () => {
|
||
if (!user) return;
|
||
try {
|
||
const res = await api.get('/notifications');
|
||
const incoming = res.data.notifications || [];
|
||
setNotifications(incoming);
|
||
setUnreadCount(res.data.unreadCount || 0);
|
||
|
||
// First fetch: just seed the seen-set without toasting
|
||
if (!initialized.current) {
|
||
incoming.forEach(n => seenIds.current.add(n.id));
|
||
initialized.current = true;
|
||
return;
|
||
}
|
||
|
||
// Subsequent fetches: toast new unread notifications
|
||
const newItems = incoming.filter(n => !n.read && !seenIds.current.has(n.id));
|
||
newItems.forEach(n => {
|
||
seenIds.current.add(n.id);
|
||
const icon = notificationIcon(n.type);
|
||
toast(`${icon} ${n.title}`, { duration: 5000 });
|
||
});
|
||
} catch {
|
||
/* silent – server may not be reachable */
|
||
}
|
||
}, [user]);
|
||
|
||
useEffect(() => {
|
||
if (!user) {
|
||
setNotifications([]);
|
||
setUnreadCount(0);
|
||
seenIds.current = new Set();
|
||
initialized.current = false;
|
||
return;
|
||
}
|
||
fetch();
|
||
const interval = setInterval(fetch, 30_000);
|
||
return () => clearInterval(interval);
|
||
}, [user, fetch]);
|
||
|
||
const markRead = async (id) => {
|
||
try {
|
||
await api.post(`/notifications/${id}/read`);
|
||
setNotifications(prev =>
|
||
prev.map(n => (n.id === id ? { ...n, read: 1 } : n)),
|
||
);
|
||
setUnreadCount(prev => Math.max(0, prev - 1));
|
||
} catch { /* silent */ }
|
||
};
|
||
|
||
const markAllRead = async () => {
|
||
try {
|
||
await api.post('/notifications/read-all');
|
||
setNotifications(prev => prev.map(n => ({ ...n, read: 1 })));
|
||
setUnreadCount(0);
|
||
} catch { /* silent */ }
|
||
};
|
||
|
||
return (
|
||
<NotificationContext.Provider value={{ notifications, unreadCount, markRead, markAllRead, refresh: fetch }}>
|
||
{children}
|
||
</NotificationContext.Provider>
|
||
);
|
||
}
|
||
|
||
export function useNotifications() {
|
||
const ctx = useContext(NotificationContext);
|
||
if (!ctx) throw new Error('useNotifications must be used within NotificationProvider');
|
||
return ctx;
|
||
}
|
||
|
||
function notificationIcon(type) {
|
||
switch (type) {
|
||
case 'room_share_added': return '🔗';
|
||
case 'room_share_removed': return '🚫';
|
||
case 'federation_invite_received': return '📩';
|
||
default: return '🔔';
|
||
}
|
||
}
|