diff --git a/src/components/screens/NotificationsScreen.tsx b/src/components/screens/NotificationsScreen.tsx
index 650fc35..40f9a58 100644
--- a/src/components/screens/NotificationsScreen.tsx
+++ b/src/components/screens/NotificationsScreen.tsx
@@ -1,16 +1,21 @@
"use client";
-import { BellOff } from "lucide-react";
+import { AnimatePresence, motion } from "framer-motion";
+import { BellOff, ChevronLeft, Trash2, X } from "lucide-react";
import { useEffect } from "react";
import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader";
-import { useNotifStore } from "@/lib/notification-store";
+import { openNotification, useNotifStore } from "@/lib/notification-store";
import { useI18n } from "@/lib/i18n";
+import { AppNotification } from "@/lib/online/types";
export function NotificationsScreen() {
const { t, locale } = useI18n();
const items = useNotifStore((s) => s.items);
+ const remove = useNotifStore((s) => s.remove);
+ const clearAll = useNotifStore((s) => s.clearAll);
const markAllRead = useNotifStore((s) => s.markAllRead);
+ // Opening the list clears the bell badge; per-item navigate/dismiss still work.
useEffect(() => {
markAllRead();
}, [markAllRead]);
@@ -23,7 +28,21 @@ export function NotificationsScreen() {
return (
-
+ 0 ? (
+
+ ) : undefined
+ }
+ />
+
{items.length === 0 && (
@@ -32,24 +51,89 @@ export function NotificationsScreen() {
{t("notif.empty")}
)}
+
+ {items.length > 0 && (
+
+
+ {t("notif.swipeHint")}
+
+ )}
+
- {items.map((n) => (
-
-
{n.icon}
-
-
- {locale === "fa" ? n.titleFa : n.titleEn}
-
- {(n.bodyFa || n.bodyEn) && (
-
- {locale === "fa" ? n.bodyFa : n.bodyEn}
-
- )}
-
-
{fmtTime(n.ts)}
-
- ))}
+
+ {items.map((n) => (
+ openNotification(n)}
+ onRemove={() => remove(n.id)}
+ />
+ ))}
+
);
}
+
+function NotifRow({
+ n,
+ locale,
+ time,
+ hint,
+ onOpen,
+ onRemove,
+}: {
+ n: AppNotification;
+ locale: string;
+ time: string;
+ hint?: string;
+ onOpen: () => void;
+ onRemove: () => void;
+}) {
+ return (
+
{
+ if (Math.abs(info.offset.x) > 90 || Math.abs(info.velocity.x) > 500) onRemove();
+ }}
+ whileDrag={{ cursor: "grabbing" }}
+ className="relative"
+ >
+
+
+
+ );
+}
diff --git a/src/lib/i18n.tsx b/src/lib/i18n.tsx
index 5877f85..e638e82 100644
--- a/src/lib/i18n.tsx
+++ b/src/lib/i18n.tsx
@@ -320,6 +320,9 @@ const fa: Dict = {
"notif.title": "اعلانها",
"notif.empty": "اعلانی ندارید",
+ "notif.clearAll": "پاک کردن همه",
+ "notif.swipeHint": "برای حذف، اعلان را به کناری بکشید",
+ "notif.tapToOpen": "برای مشاهده بزنید",
"settings.audio": "تنظیمات صدا",
"settings.sound": "افکت صدا",
@@ -653,6 +656,9 @@ const en: Dict = {
"notif.title": "Notifications",
"notif.empty": "No notifications yet",
+ "notif.clearAll": "Clear all",
+ "notif.swipeHint": "Swipe a notification aside to dismiss it",
+ "notif.tapToOpen": "Tap to open",
"settings.audio": "Audio",
"settings.sound": "Sound effects",
diff --git a/src/lib/notification-store.ts b/src/lib/notification-store.ts
index 62d5195..30146d5 100644
--- a/src/lib/notification-store.ts
+++ b/src/lib/notification-store.ts
@@ -4,6 +4,7 @@ import { create } from "zustand";
import { getService } from "./online/service";
import { AppNotification } from "./online/types";
import { sound } from "./sound";
+import { Screen, useUIStore } from "./ui-store";
interface NotifStore {
items: AppNotification[];
@@ -11,6 +12,9 @@ interface NotifStore {
lastToast: AppNotification | null;
add: (n: AppNotification) => void;
markAllRead: () => void;
+ markRead: (id: string) => void;
+ remove: (id: string) => void;
+ clearAll: () => void;
dismissToast: () => void;
init: () => void;
}
@@ -18,6 +22,8 @@ interface NotifStore {
let unsub: (() => void) | null = null;
let started = false;
+const countUnread = (items: AppNotification[]) => items.filter((x) => !x.read).length;
+
export const useNotifStore = create
((set, get) => ({
items: [],
unread: 0,
@@ -25,13 +31,26 @@ export const useNotifStore = create((set, get) => ({
add: (n) => {
const items = [n, ...get().items].slice(0, 50);
- set({ items, unread: items.filter((x) => !x.read).length, lastToast: n });
+ set({ items, unread: countUnread(items), lastToast: n });
sound.play("notify");
},
markAllRead: () =>
set({ items: get().items.map((x) => ({ ...x, read: true })), unread: 0 }),
+ markRead: (id) => {
+ const items = get().items.map((x) => (x.id === id ? { ...x, read: true } : x));
+ set({ items, unread: countUnread(items) });
+ },
+
+ remove: (id) => {
+ const items = get().items.filter((x) => x.id !== id);
+ const lastToast = get().lastToast?.id === id ? null : get().lastToast;
+ set({ items, unread: countUnread(items), lastToast });
+ },
+
+ clearAll: () => set({ items: [], unread: 0, lastToast: null }),
+
dismissToast: () => set({ lastToast: null }),
init: () => {
@@ -50,4 +69,38 @@ export function pushNotification(n: Omit)
});
}
+/** Default destination for a notification kind when no explicit `route` is set. */
+function defaultRoute(kind: AppNotification["kind"]): Screen | "daily" | null {
+ switch (kind) {
+ case "friend_request":
+ case "invite":
+ return "friends";
+ case "achievement":
+ case "reward":
+ return "achievements";
+ case "daily":
+ return "daily"; // opens the daily-reward modal
+ case "system":
+ default:
+ return null;
+ }
+}
+
+/**
+ * Act on a tapped notification: mark it read and deep-link to its related screen.
+ * `daily` opens the daily-reward modal (mounted globally) over the current screen.
+ */
+export function openNotification(n: AppNotification) {
+ useNotifStore.getState().markRead(n.id);
+ useNotifStore.getState().dismissToast();
+ const target = (n.route as Screen | "daily" | undefined) ?? defaultRoute(n.kind);
+ if (!target) return;
+ const ui = useUIStore.getState();
+ if (target === "daily") {
+ ui.openDaily();
+ return;
+ }
+ ui.go(target);
+}
+
void unsub;
diff --git a/src/lib/online/types.ts b/src/lib/online/types.ts
index 46d5f6b..300d181 100644
--- a/src/lib/online/types.ts
+++ b/src/lib/online/types.ts
@@ -593,6 +593,7 @@ export type NotificationKind =
| "friend_request"
| "invite"
| "achievement"
+ | "reward"
| "daily"
| "system";
@@ -606,6 +607,10 @@ export interface AppNotification {
icon: string; // emoji
ts: number;
read: boolean;
+ /** explicit deep-link target screen name (overrides the kind default) */
+ route?: string;
+ /** optional payload, e.g. a user id (friend_request) the target screen can use */
+ payload?: string;
}
/* ------------------------------ Avatars ------------------------------ */