mirror of
https://github.com/layer-systems/website.git
synced 2026-09-12 13:43:01 +02:00
* feat: web bookmarks for arbitrary URLs (NIP-B0) Adds a Web Bookmarks app backed by NIP-B0 (kind 39701): one addressable event per saved URL, distinct from the NIP-51 bookmark list (#21) since it carries its own title/description/tags per page rather than being an entry in a list. - src/hooks/useWebBookmarks.ts: create/list/delete, plus bookmarkDTag/bookmarkUrl implementing the spec's "strip https://" d-tag rule (round-tripped by a unit test). - Delete publishes a NIP-09 kind 5 request and also drops the item from the local query cache directly, since relays aren't obligated to honor the deletion. - New src/apps/web-bookmarks/index.tsx: inline add form, list with title/description/tags, opens the saved URL in a new tab. Closes #25 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto * fix: address review feedback on web bookmarks Per review: - bookmarkDTag() now matches the https scheme case-insensitively, so "HTTPS://…" and "https://…" collapse to the same d tag instead of creating duplicate bookmarks. - The form now accepts every scheme sanitizeUrl() allows (https, http, mailto, nostr) via a dedicated isBookmarkableUrl() check — not sanitizeUrl() itself, which resolves relative URLs against this app's own origin and would have "validated" a bare hostname like "example.com" as a link back into the app. - WebBookmarkRow no longer falls back to the raw unsanitized URL when sanitizeUrl() rejects it (e.g. a malicious "d" tag) — it renders plain text with no link instead of defeating the sanitization. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto * fix: round-trip mailto:/nostr: bookmarks and preserve published_at Per review: - bookmarkUrl() required "scheme://" to recognize an already-schemed d tag, so opaque URIs with no "//" — mailto: and nostr: — were incorrectly prefixed with "https://". Fixed by also checking a closed list of the opaque schemes this app supports, alongside the existing "://" check (kept as-is so a hierarchical scheme like gemini:// still round-trips, and so a stripped https URL containing a port, e.g. alice.blog:8080/post, still isn't misread as scheme "alice.blog"). Added regression tests for all three cases. - useCreateWebBookmark now looks up the existing bookmark for the same d tag before publishing and carries its published_at forward, instead of resetting it to now on every edit — per NIP-B0, published_at is "the first time the bookmark was published." Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto * fix: dedupe multiple revisions of the same web bookmark Per an earlier "previously missed" finding: useMyWebBookmarks() returned every kind-39701 event a relay handed back, but for an addressable event the pool can return more than one revision of the same d tag (an edit history, or relays disagreeing on what's current), which showed up as duplicate rows for the same URL. Extracted dedupeLatestByDTag() (keeps the newest per d, newest-first) and covered it with regression tests. 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.