* feat: local draft notes with a blank "new note" entry point
Writing was tied to publishing: the Feed composer either sits empty
or fires a note straight to relays, with nowhere to keep something
you're not ready to publish yet.
The Note app now supports a draft mode when opened without an id: a
blank note kept in localStorage until you publish it or discard it,
reachable via a new "New note" button in the Feed toolbar, the Go
menu, or the command palette (all already open the Note app with no
params).
Closes#19
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
* fix: guard against double-publish and dropped relay params
Per review:
- handlePublish now also checks publish.isPending itself, not just
the button's disabled state — a second click landing before React
re-renders could otherwise fire mutateAsync twice.
- Publishing a draft now merges into the existing params instead of
replacing them outright, so relay hints (or anything else already
in params) survive the id being added.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
* docs: mark notes app's id param as optional
Per review — the draft mode added by this PR means id is no longer
required to open the Note app.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
* fix: sync useLocalStorage across same-tab consumers of one key
Per review: the Notes app is explicitly non-singleton, so opening two
"New Note" windows meant two DraftNote instances writing the same
localStorage key independently — the native `storage` event only
fires in *other* tabs/documents, never the one that wrote, so the two
windows would silently diverge (discard/publish in one wouldn't
update the other).
useLocalStorage now also dispatches a same-document custom event on
every write, and every instance sharing that key listens for it —
verified live with two open draft windows staying in sync as one is
typed into.
Also dropped a redundant `{}` params argument on an openApp() call
that every other call site omits when opening with no parameters.
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>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* fix: exclude replies from the Feed and Profile timelines
Per NIP-10 a kind-1 event with an `e` tag is a reply, but the Feed and
Profile timelines rendered every kind-1 event with no such check, so
replies showed up indistinguishable from root posts. `isReply()` was
already written for this in nostrUtils but never used anywhere.
Fixes#20
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
* fix: don't treat mention-only e tags as replies
Per review: isReply() flagged any e tag as a reply, including one
marked "mention" — a citation, not a thread reply per NIP-10. That
would have hidden quote-notes from the Feed/Profile timelines they
belong in. Also fixed rootReference()'s docstring, which claimed to
fall back to the *last* positional e tag when the code (correctly)
uses the first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
* fix: rootReference() no longer treats a mention as the root
Follow-up to the isReply() fix: rootReference()'s positional fallback
still matched any e tag regardless of marker, so an event with only a
mention-marked e tag would incorrectly return the mentioned id as the
thread root. The fallback now only considers unmarked e tags, per the
deprecated positional NIP-10 scheme.
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>
* new web os frontend
* add docs
* Add CNAME and restore NIP-05 nostr.json for GitHub Pages
The Pages custom domain (layer.systems) is only stored in repo settings;
a CNAME file in the build output makes it survive Pages reconfiguration.
Restore public/.well-known/nostr.json, which this branch had dropped —
removing it would break the existing NIP-05 identifiers on layer.systems.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014YtQoCzkP7Bo8nruhxojPi
* Ignore eslint and tsc build caches
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014YtQoCzkP7Bo8nruhxojPi
* Rebrand page title and metadata to LAYER.systems
The site ships on layer.systems, so the document title, meta and OG
description, and the web manifest now carry that name instead of
"Nostr OS". OsShell sets the title at runtime, so it is updated too —
otherwise the tab would fall back to the old branding after hydration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014YtQoCzkP7Bo8nruhxojPi
* Rename remaining visible "Nostr OS" strings to LAYER.systems
Covers the About window heading, the mobile shell header and the app
icon's aria-label, so the visible branding matches the page title.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014YtQoCzkP7Bo8nruhxojPi
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>