Add in-app + real-time notifications (SignalR/mock, Iran-friendly)

- AppNotification + OnlineService.onNotification (hub event + mock periodic) —
  no FCM/APNs (blocked in Iran); uses the existing realtime channel
- notification-store + pushNotification(); 🔔 bell with unread badge in TopBar,
  notifications screen, global toaster (plays notify sfx)
- Wired events: daily reward, post-match achievements, friend requests
- Closed-app push (Pushe/Najva/Chabok) noted as a later step (needs provider keys)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 15:52:06 +03:30
parent e02d976dda
commit 2d2352dfe8
13 changed files with 291 additions and 3 deletions
+15 -1
View File
@@ -1,8 +1,9 @@
"use client";
import { Coins, Crown, Gift } from "lucide-react";
import { Bell, Coins, Crown, Gift } from "lucide-react";
import { useSessionStore } from "@/lib/session-store";
import { useUIStore } from "@/lib/ui-store";
import { useNotifStore } from "@/lib/notification-store";
import { useI18n } from "@/lib/i18n";
import { Avatar } from "./Avatar";
@@ -10,6 +11,7 @@ export function TopBar() {
const profile = useSessionStore((s) => s.profile);
const go = useUIStore((s) => s.go);
const openDaily = useUIStore((s) => s.openDaily);
const unread = useNotifStore((s) => s.unread);
const { t } = useI18n();
if (!profile) return null;
@@ -34,6 +36,18 @@ export function TopBar() {
</button>
<div className="flex items-center gap-2">
<button
onClick={() => go("notifications")}
className="glass rounded-full p-2 hover:bg-navy-800/80 transition relative"
title={t("notif.title")}
>
<Bell className="size-4 text-gold-400" />
{unread > 0 && (
<span className="absolute -top-0.5 ltr:-right-0.5 rtl:-left-0.5 min-w-4 h-4 px-1 rounded-full bg-rose-500 text-[9px] font-bold text-white flex items-center justify-center">
{unread > 9 ? "9+" : unread}
</span>
)}
</button>
<button
onClick={openDaily}
className="glass rounded-full p-2 hover:bg-navy-800/80 transition"