haixunMaster/lib/notifications/store.ts

142 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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));
}