import { create } from "zustand"; import { apiFetch } from "../lib/api"; import { supabase } from "../lib/supabase"; import type { RealtimeChannel } from "@supabase/supabase-js"; export interface AppNotification { id: string; type: string; actorName: string; actorAvatar: string | null; postId: string | null; preview: string | null; readAt: string | null; createdAt: string; } type NotificationState = { items: AppNotification[]; unread: number; loaded: boolean; load: () => Promise; markRead: () => Promise; remove: (id: string) => Promise; startRealtime: () => Promise; stopRealtime: () => void; reset: () => void; }; let realtimeSub: RealtimeChannel | null = null; let reconnectTimer: ReturnType | null = null; export const useNotificationStore = create((set, get) => ({ items: [], unread: 0, loaded: false, load: async () => { try { const res = await apiFetch<{ items: AppNotification[]; unread: number }>( "/api/notifications", ); set({ items: res.items ?? [], unread: res.unread ?? 0, loaded: true }); } catch (err) { console.warn("[notifications] load failed:", err); } }, markRead: async () => { if (get().unread === 0) return; const now = new Date().toISOString(); set((s) => ({ unread: 0, items: s.items.map((n) => ({ ...n, readAt: n.readAt ?? now })), })); try { await apiFetch("/api/notifications/read", { method: "POST" }); } catch (err) { console.warn("[notifications] markRead failed:", err); } }, remove: async (id) => { set((s) => ({ items: s.items.filter((n) => n.id !== id) })); try { await apiFetch(`/api/notifications/${id}`, { method: "DELETE" }); } catch (err) { console.warn("[notifications] remove failed:", err); } }, startRealtime: async () => { if (realtimeSub) return; const { data } = await supabase.auth.getSession(); const session = data.session; if (!session?.user?.id) return; supabase.realtime.setAuth(session.access_token); const myId = session.user.id; realtimeSub = supabase .channel(`notifications:${myId}:${Date.now()}`) .on( "postgres_changes", { event: "INSERT", schema: "rebreak", table: "notifications", filter: `recipient_id=eq.${myId}`, }, (payload: any) => { const r = payload.new; const notif: AppNotification = { id: r.id, type: r.type, actorName: r.actor_name, actorAvatar: r.actor_avatar ?? null, postId: r.post_id ?? null, preview: r.preview ?? null, readAt: null, createdAt: r.created_at, }; set((s) => { if (s.items.find((n) => n.id === notif.id)) return s; return { items: [notif, ...s.items], unread: s.unread + 1 }; }); }, ) .on( "postgres_changes", { event: "DELETE", schema: "rebreak", table: "notifications", filter: `recipient_id=eq.${myId}`, }, (payload: any) => { const id = payload.old?.id; if (!id) return; set((s) => ({ items: s.items.filter((n) => n.id !== id) })); }, ) .subscribe((status) => { if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") { console.warn("[notifRealtime] error:", status); get().stopRealtime(); if (reconnectTimer) clearTimeout(reconnectTimer); reconnectTimer = setTimeout(() => { get().startRealtime(); }, 3000); } }); }, stopRealtime: () => { if (realtimeSub) { supabase.removeChannel(realtimeSub); realtimeSub = null; } if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } }, reset: () => { get().stopRealtime(); set({ items: [], unread: 0, loaded: false }); }, }));