1"use client";
2
3
21import { useEffect, useMemo, useRef, useState } from "react";
22import { useQuery, useQueryClient } from "@tanstack/react-query";
23import { NOTIFICATION_POLICY } from "@factory/shared/realtime/notification-policy";
24import { notificationApi } from "@/lib/api";
25import { realtime } from "@/lib/realtime";
26import { useAuthStore } from "@/lib/auth-store";
27import { toast } from "@/stores/ui-store";
28import type { Notification } from "@/lib/types";
29
30
34const TOAST_KINDS = new Set(
35 Object.values(NOTIFICATION_POLICY)
36 .filter((p) => p.toast)
37 .map((p) => p.kind)
38);
39
40export interface NotificationsState {
41
42 unreadCount: number;
43 isLoading: boolean;
44
45 invalidate: () => void;
46
47 frameToasts: (kind: string) => boolean;
48}
49
50const POLL_MS = 30_000;
51
52export function useNotifications(): NotificationsState {
53 const qc = useQueryClient();
54 const userId = useAuthStore((s) => s.user?.id ?? null);
55 const [count, setCount] = useState(0);
56 const [ready, setReady] = useState(false);
57
58
59 const query = useQuery({
60 queryKey: ["notifications", "unread-count"],
61 queryFn: async () => {
62 const res = await notificationApi.unreadCount();
63 return res.count;
64 },
65 enabled: userId !== null,
66 refetchInterval: POLL_MS
67 });
68
69
70
71 useEffect(() => {
72 if (query.data !== undefined) {
73 setCount(query.data);
74 setReady(true);
75 }
76 }, [query.data]);
77
78
79
80
81
82 const lastPushed = useRef<Set<string>>(new Set());
83 useEffect(() => {
84 if (userId === null) return;
85 const off = realtime.on("notification.push", (frame) => {
86 if (frame.type !== "notification.push") return;
87
88 if (lastPushed.current.has(frame.notificationId)) return;
89 lastPushed.current.add(frame.notificationId);
90 setCount((c) => c + 1);
91 setReady(true);
92
93
94 void qc.invalidateQueries({ queryKey: ["notifications", "list"] });
95 if (TOAST_KINDS.has(frame.kind)) {
96 toast("info", frame.actorName ?? "New notification", frame.title);
97 }
98 });
99 return () => {
100 off();
101 lastPushed.current.clear();
102 };
103 }, [userId]);
104
105 const invalidate = useMemo(
106 () => () => void qc.invalidateQueries({ queryKey: ["notifications", "unread-count"] }),
107 [qc]
108 );
109
110 return useMemo(
111 () => ({
112 unreadCount: count,
113 isLoading: !ready,
114 invalidate,
115 frameToasts: (kind: string) => TOAST_KINDS.has(kind)
116 }),
117 [count, ready, invalidate]
118 );
119}
120
121
122export function useNotificationList() {
123 return useQuery({
124 queryKey: ["notifications", "list", "all"],
125 queryFn: async () => (await notificationApi.list({ limit: 50 })).data
126 });
127}
128
129export type { Notification };
130