Files
website/docs
mroxso 179d3682f8 feat: bookmarks for notes and articles (NIP-51) (#29)
* 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>
2026-09-06 18:45:21 +02:00
..
2026-09-06 11:37:43 +02:00
2026-09-06 11:37:43 +02:00
2026-09-06 11:37:43 +02:00

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.