2026-10-07 19:40:26 +03:30
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useEffect, useRef } from "react";
|
2026-10-10 16:42:35 +03:30
|
|
|
import { useRouter } from "next/navigation";
|
2026-10-07 19:40:26 +03:30
|
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
import { useAuth } from "@/lib/auth-store";
|
|
|
|
|
import { getWsUrl } from "@/lib/api";
|
|
|
|
|
import { toast } from "@/components/providers";
|
|
|
|
|
|
|
|
|
|
interface WSPayload {
|
|
|
|
|
type?: string;
|
|
|
|
|
item?: {
|
|
|
|
|
type?: string;
|
|
|
|
|
title?: string;
|
|
|
|
|
body?: string;
|
|
|
|
|
category?: string;
|
|
|
|
|
route?: string;
|
|
|
|
|
};
|
|
|
|
|
from?: number;
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Connects to the realtime gateway, toasts live notifications and bumps unread counts. */
|
|
|
|
|
export function useRealtimeGateway() {
|
|
|
|
|
const accessToken = useAuth((s) => s.accessToken);
|
|
|
|
|
const user = useAuth((s) => s.user);
|
|
|
|
|
const client = useQueryClient();
|
2026-10-10 16:42:35 +03:30
|
|
|
const router = useRouter();
|
2026-10-07 19:40:26 +03:30
|
|
|
const lastEvent = useRef<Record<string, number>>({});
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!user) return;
|
|
|
|
|
let ws: WebSocket | null = null;
|
|
|
|
|
let retry = 0;
|
|
|
|
|
let closed = false;
|
|
|
|
|
|
|
|
|
|
const connect = () => {
|
|
|
|
|
if (closed) return;
|
|
|
|
|
try {
|
|
|
|
|
// Prefer subprotocol when we have an in-memory access token; otherwise rely on httpOnly cookie.
|
|
|
|
|
ws = accessToken
|
|
|
|
|
? new WebSocket(getWsUrl(), ["bearer", accessToken])
|
|
|
|
|
: new WebSocket(getWsUrl());
|
|
|
|
|
} catch {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
ws.onopen = () => {
|
|
|
|
|
retry = 0;
|
|
|
|
|
};
|
|
|
|
|
ws.onmessage = (ev) => {
|
|
|
|
|
try {
|
|
|
|
|
const p: WSPayload = JSON.parse(ev.data);
|
|
|
|
|
if (p.type === "notification" && p.item) {
|
|
|
|
|
const n = p.item;
|
|
|
|
|
const stamp = Date.now();
|
|
|
|
|
const dedupeKey = `${n.type}:${n.title}:${n.body}`;
|
|
|
|
|
if (stamp - (lastEvent.current[dedupeKey] ?? 0) < 2000) return;
|
|
|
|
|
lastEvent.current[dedupeKey] = stamp;
|
|
|
|
|
|
2026-10-10 16:42:35 +03:30
|
|
|
const title = n.title?.trim() || "اعلان";
|
|
|
|
|
const body = n.body?.trim();
|
|
|
|
|
toast.custom(
|
|
|
|
|
(id) => (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
dir="rtl"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
toast.dismiss(id);
|
|
|
|
|
if (n.route) router.push(n.route);
|
|
|
|
|
}}
|
2026-10-18 10:40:13 +03:30
|
|
|
className="flex w-[min(22rem,calc(100vw-1.5rem))] items-stretch overflow-hidden rounded-xl border border-border bg-card text-start shadow-lg"
|
2026-10-10 16:42:35 +03:30
|
|
|
>
|
2026-10-18 10:40:13 +03:30
|
|
|
<span className="w-1 shrink-0 bg-primary" aria-hidden />
|
|
|
|
|
<span className="flex min-w-0 flex-col px-3 py-2.5">
|
|
|
|
|
<span dir="auto" className="block truncate text-sm font-semibold text-foreground [unicode-bidi:plaintext]">
|
|
|
|
|
{title}
|
2026-10-10 16:42:35 +03:30
|
|
|
</span>
|
2026-10-18 10:40:13 +03:30
|
|
|
{body ? (
|
|
|
|
|
<span dir="auto" className="mt-0.5 block line-clamp-2 text-xs text-muted-foreground [unicode-bidi:plaintext]">
|
|
|
|
|
{body}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</span>
|
2026-10-10 16:42:35 +03:30
|
|
|
</button>
|
|
|
|
|
),
|
|
|
|
|
{ duration: 5000 },
|
|
|
|
|
);
|
2026-10-07 19:40:26 +03:30
|
|
|
client.invalidateQueries({ queryKey: ["notifications"] });
|
|
|
|
|
client.invalidateQueries({ queryKey: ["me"] });
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
/* ignore malformed frames */
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
ws.onclose = () => {
|
|
|
|
|
if (closed) return;
|
|
|
|
|
retry += 1;
|
|
|
|
|
const t = Math.min(1000 * 2 ** retry, 15000);
|
|
|
|
|
setTimeout(connect, t);
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
connect();
|
|
|
|
|
return () => {
|
|
|
|
|
closed = true;
|
|
|
|
|
ws?.close();
|
|
|
|
|
};
|
2026-10-10 16:42:35 +03:30
|
|
|
}, [accessToken, user, client, router]);
|
2026-10-07 19:40:26 +03:30
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|