mirror of
https://github.com/hoornet/vega.git
synced 2026-04-24 06:40:01 -07:00
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>
86 lines
2.6 KiB
TypeScript
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 });
|
|
},
|
|
}));
|