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>
This commit is contained in:
Jure
2026-03-11 20:39:30 +01:00
parent 181233796b
commit 3a196cb9a0
22 changed files with 479 additions and 63 deletions

View File

@@ -0,0 +1,42 @@
import { useEffect } from "react";
import { useUIStore } from "../stores/ui";
import { useFeedStore } from "../stores/feed";
export function useKeyboardShortcuts() {
const { currentView, setView, goBack, toggleHelp } = useUIStore();
const { focusedNoteIndex, setFocusedNoteIndex, notes } = useFeedStore();
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const tag = (e.target as HTMLElement).tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || (e.target as HTMLElement).isContentEditable) return;
switch (e.key) {
case "n":
setView("feed");
setTimeout(() => (document.querySelector("[data-compose]") as HTMLTextAreaElement)?.focus(), 50);
break;
case "/":
e.preventDefault();
setView("search");
setTimeout(() => (document.querySelector("[data-search-input]") as HTMLInputElement)?.focus(), 50);
break;
case "Escape":
goBack();
break;
case "?":
toggleHelp();
break;
case "j":
if (currentView === "feed")
setFocusedNoteIndex(Math.min(focusedNoteIndex + 1, notes.length - 1));
break;
case "k":
if (currentView === "feed")
setFocusedNoteIndex(Math.max(focusedNoteIndex - 1, 0));
break;
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [currentView, focusedNoteIndex, notes.length]);
}