haixunMaster/lib/notifications/store.ts

142 lines
3.6 KiB
TypeScript
Raw Permalink Normal View History

2026-06-21 12:50:31 +00:00
export type NotificationType = "success" | "error" | "info" | "warning";
export interface AppNotification {
id: string;
type: NotificationType;
title: string;
message?: string;
href?: string;
read: boolean;
createdAt: string;
}
const STORAGE_KEY_PREFIX = "haixun-notifications";
const MAX_ITEMS = 80;
type Listener = () => void;
const listeners = new Set<Listener>();
let scopedUserId: string | null = null;
let cachedNotifications: AppNotification[] = [];
let cachedUnreadCount = 0;
let initialized = false;
function storageKey() {
return scopedUserId ? `${STORAGE_KEY_PREFIX}-${scopedUserId}` : `${STORAGE_KEY_PREFIX}-anonymous`;
}
function emit() {
listeners.forEach((fn) => fn());
}
/** 切換登入使用者時重載通知,避免看到別人的 local 紀錄。 */
export function setNotificationScope(userId: string) {
if (scopedUserId === userId) return;
scopedUserId = userId;
initialized = false;
cachedNotifications = [];
cachedUnreadCount = 0;
ensureInitialized();
}
function load(): AppNotification[] {
if (typeof window === "undefined") return [];
try {
const raw = localStorage.getItem(storageKey());
if (!raw) return [];
return JSON.parse(raw) as AppNotification[];
} catch {
return [];
}
}
function refreshUnreadCount() {
cachedUnreadCount = cachedNotifications.filter((n) => !n.read).length;
}
function ensureInitialized() {
if (initialized || typeof window === "undefined") return;
cachedNotifications = load();
refreshUnreadCount();
initialized = true;
}
function createNotificationId(): string {
const c = typeof globalThis !== "undefined" ? globalThis.crypto : undefined;
if (c && typeof c.randomUUID === "function") {
return c.randomUUID();
}
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
}
function save(items: AppNotification[]) {
cachedNotifications = items.slice(0, MAX_ITEMS);
refreshUnreadCount();
initialized = true;
if (typeof window !== "undefined") {
try {
localStorage.setItem(storageKey(), JSON.stringify(cachedNotifications));
} catch {
// localStorage 已滿或不可寫入(例如 Safari 隱私模式),略過持久化
}
}
emit();
}
export function subscribe(listener: Listener): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function getNotifications(): AppNotification[] {
ensureInitialized();
return cachedNotifications;
}
export function getUnreadCount(): number {
ensureInitialized();
return cachedUnreadCount;
}
/**
* AI
* InlineAlertuseActionFeedback
*/
export function notify(params: {
type: NotificationType;
title: string;
message?: string;
href?: string;
}) {
ensureInitialized();
const item: AppNotification = {
id: createNotificationId(),
type: params.type,
title: params.title,
message: params.message,
href: params.href,
read: false,
createdAt: new Date().toISOString(),
};
save([item, ...cachedNotifications]);
return item.id;
}
export function markRead(id: string) {
ensureInitialized();
save(cachedNotifications.map((n) => (n.id === id ? { ...n, read: true } : n)));
}
export function markAllRead() {
ensureInitialized();
save(cachedNotifications.map((n) => ({ ...n, read: true })));
}
export function clearAll() {
save([]);
}
export function removeNotification(id: string) {
ensureInitialized();
save(cachedNotifications.filter((n) => n.id !== id));
}