mirror of
https://github.com/layer-systems/website.git
synced 2026-09-12 13:43:01 +02:00
* feat: bookmarks for notes and articles (NIP-51) Adds a Bookmarks app backed by a kind 10003 NIP-51 bookmark list: - BookmarkButton toggles a note (`e` tag) or article (`a` tag) in and out of the signed-in user's list, reading it back before publishing so an update never clobbers other entries — the same whole-list replacement trap follow lists have. - Wired into NoteCard's action row and the Reader's article toolbar. - The new Bookmarks app lists saved notes and articles, opening articles back in the Reader. Closes #21 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto * fix: address review feedback and add a Bookmarked tab to the Reader Per review: - useToggleBookmark now fetches the bookmark list fresh from relays right before writing instead of trusting the query cache (60s staleTime), which could otherwise clobber concurrent edits from another tab or device. - The article BookmarkButton only renders when the article actually has a `d` tag, instead of falling back to an unresolvable "kind:pubkey:" address. - Bookmarked note/article ids are filtered for a non-empty tag value before use, and article addresses are parsed properly (kind, author, `d`) instead of a naive split(':')[2] — the relay query is now also constrained by kind and author, not just `d`, and identifiers containing ':' round-trip correctly. - BookmarkButton sets type="button" so it can't misbehave as a form submit button. Per a reviewer comment: added a "Recent" / "Bookmarked" tab to the Reader's sidebar (src/apps/articles/index.tsx) so bookmarked articles are reachable without leaving the app — the dedicated Bookmarks app stays as-is. Both now share useMyBookmarkedArticles from src/hooks/useBookmarks.ts rather than duplicating the address-parsing logic. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto * fix: reject empty-identifier addresses and surface bookmark load errors Per review: - parseAddress() now rejects an empty d-identifier as malformed (e.g. "30023:<pubkey>:") instead of producing a "#d: ['']" relay query and an unopenable bookmark. - useMyBookmarkedArticles() filters out matched events with empty content, the same non-renderable criteria the Reader's own list uses, so a broken/blank article can't land in the Bookmarked view. - BookmarksApp now distinguishes "the query failed" from "there are no bookmarks" — React Query leaves data undefined in both cases, so a relay/network failure no longer reads as an empty list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto --------- Co-authored-by: highperfocused <highperfocused@pm.me> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Documentation
This project is a Nostr client built as a desktop operating system: there are no
pages, only apps that open as windows you can move, resize, stack and keep side by
side. PLAN.md in the repository root records why it was built this way and which
decisions were taken; these documents describe how it actually works.
| Document | What it covers |
|---|---|
window-manager.md |
The OS core: state, geometry, gestures, persistence, routing |
apps.md |
The app registry, the contract every app implements, and how to add one |
nostr.md |
Data access, relay hints, and the rules for rendering untrusted content |
styleguide.md |
Design tokens, materials, typography, motion, and the layout rules for app content |
Layout of the custom code
src/
os/ # Window manager — no UI, no Nostr
types.ts # AppDefinition, WindowState, AppProps
registry.ts # The catalogue of apps
windowReducer.ts # All state transitions (pure)
WindowManagerProvider.tsx
WindowManagerContext.ts
useWindowManager.ts
layout.ts # Viewport maths: clamping, cascade, snap targets
useDrag.ts # Pointer-driven moving
useResize.ts # Pointer-driven resizing
useOsKeyboard.ts # System-wide shortcuts
persistence.ts # Session save / restore
components/os/ # The shell
OsShell.tsx # Entry point: picks desktop or mobile, syncs the URL
MenuBar.tsx # macOS-style bar (app menu, Go, Window, relays, theme, login)
MenuBarClock.tsx
Desktop.tsx # Wallpaper, icon grid, desktop context menu
DesktopIcon.tsx
WindowLayer.tsx # Renders every window, owns the snap preview
WindowFrame.tsx # Window chrome: title bar, traffic lights, resize handles
TrafficLights.tsx
CommandPalette.tsx # ⌘K
MobileAppShell.tsx # Home screen + full-screen app, under 768px
AppChrome.tsx # Layout primitives every app builds on
components/nostr/ # Shared Nostr UI
NoteCard.tsx, NoteContent.tsx, AuthorLine.tsx, LoginRequired.tsx
apps/<id>/index.tsx # One default-exported component per app
hooks/ # useRelayStatus, useRelayHints, useFollows (+ template hooks)
lib/nostrUtils.ts # sanitizeUrl, relay hints, tag helpers, time formatting
The three layers do not reach into each other: src/os/ knows nothing about Nostr or
about any particular app, components/os/ knows about windows but not about note
kinds, and an app knows about its own data but never about window geometry.
Running it
npm run dev # Vite dev server (port 8080 by default — see vite.config.ts)
npm run test # tsc --noEmit + eslint + vitest + production build
npm run test is the gate: it type-checks, lints, runs the unit tests and builds.
Nothing is finished until it passes.