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
+14
View File
@@ -2,6 +2,7 @@
import { create } from "zustand";
import { CreateRoomOptions, MatchmakingOptions, getService } from "./online/service";
import { pushNotification } from "./notification-store";
import {
ChatMessage,
Friend,
@@ -49,6 +50,7 @@ let roomUnsub: (() => void) | null = null;
let mmUnsub: (() => void) | null = null;
let friendUnsub: (() => void) | null = null;
let chatUnsub: (() => void) | null = null;
const seenRequests = new Set<string>();
export const useOnlineStore = create<OnlineStore>((set, get) => ({
friends: [],
@@ -61,6 +63,18 @@ export const useOnlineStore = create<OnlineStore>((set, get) => ({
const svc = getService();
const [friends, requests] = await Promise.all([svc.listFriends(), svc.listRequests()]);
set({ friends, requests });
for (const r of requests) {
if (seenRequests.has(r.id)) continue;
seenRequests.add(r.id);
pushNotification({
kind: "friend_request",
titleFa: "درخواست دوستی جدید",
titleEn: "New friend request",
bodyFa: r.from.displayName,
bodyEn: r.from.displayName,
icon: "👥",
});
}
if (!friendUnsub) friendUnsub = svc.onFriends((f) => set({ friends: f }));
},