Files
vega/src/stores/notifications.ts
Jure 3a196cb9a0 Bump to v0.2.0 — Phase 2: Engagement & Reach
Four features shipped in this release:

- Feed reply context: replies show "↩ replying to @name" above the
  note content; clicking fetches and opens the parent thread

- NIP-65 outbox model: fetchUserRelayList + publishRelayList +
  fetchUserNotesNIP65 in client.ts; profile notes fetched via the
  author's write relays; "Publish relay list to Nostr" button in
  Settings (kind 10002)

- Notifications: new store (notifications.ts) + NotificationsView;
  🔔 sidebar nav item with unread badge; DM nav item also shows
  unread conversation count; badges clear on open/select

- Keyboard shortcuts: useKeyboardShortcuts hook + HelpModal;
  n=compose, /=search, j/k=feed nav with ring highlight,
  Esc=back, ?=help overlay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 20:39:30 +01:00

86 lines
2.6 KiB
TypeScript

import { create } from "zustand";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { fetchMentions } from "../lib/nostr";
const NOTIF_SEEN_KEY = "wrystr_notif_last_seen";
const DM_SEEN_KEY = "wrystr_dm_last_seen";
interface NotificationsState {
notifications: NDKEvent[];
unreadCount: number;
lastSeenAt: number;
loading: boolean;
currentPubkey: string | null;
dmLastSeen: Record<string, number>;
dmUnreadCount: number;
fetchNotifications: (pubkey: string) => Promise<void>;
markAllRead: () => void;
markDMRead: (partnerPubkey: string) => void;
computeDMUnread: (conversations: Array<{ partnerPubkey: string; lastAt: number }>) => void;
}
function loadLastSeen(): number {
const stored = parseInt(localStorage.getItem(NOTIF_SEEN_KEY) ?? "0");
return stored || Math.floor(Date.now() / 1000) - 86400;
}
function loadDMLastSeen(): Record<string, number> {
try {
return JSON.parse(localStorage.getItem(DM_SEEN_KEY) ?? "{}");
} catch {
return {};
}
}
export const useNotificationsStore = create<NotificationsState>((set, get) => ({
notifications: [],
unreadCount: 0,
lastSeenAt: loadLastSeen(),
loading: false,
currentPubkey: null,
dmLastSeen: loadDMLastSeen(),
dmUnreadCount: 0,
fetchNotifications: async (pubkey: string) => {
const state = get();
const isNewAccount = pubkey !== state.currentPubkey;
if (isNewAccount) {
set({ notifications: [], currentPubkey: pubkey });
}
set({ loading: true });
try {
const lastSeenAt = isNewAccount ? loadLastSeen() : get().lastSeenAt;
const events = await fetchMentions(pubkey, lastSeenAt);
const unreadCount = events.filter((e) => (e.created_at ?? 0) > lastSeenAt).length;
set({ notifications: events, unreadCount, lastSeenAt });
} catch {
// Non-critical
} finally {
set({ loading: false });
}
},
markAllRead: () => {
const now = Math.floor(Date.now() / 1000);
localStorage.setItem(NOTIF_SEEN_KEY, String(now));
set({ lastSeenAt: now, unreadCount: 0 });
},
markDMRead: (partnerPubkey: string) => {
const now = Math.floor(Date.now() / 1000);
const dmLastSeen = { ...get().dmLastSeen, [partnerPubkey]: now };
localStorage.setItem(DM_SEEN_KEY, JSON.stringify(dmLastSeen));
set({ dmLastSeen });
// dmUnreadCount will be recomputed by computeDMUnread on next DM view render
},
computeDMUnread: (conversations: Array<{ partnerPubkey: string; lastAt: number }>) => {
const { dmLastSeen } = get();
const dmUnreadCount = conversations.filter(
(c) => c.lastAt > (dmLastSeen[c.partnerPubkey] ?? 0)
).length;
set({ dmUnreadCount });
},
}));