fix: don't highlight against a malformed address, gate the listener

Per a "needs a closer look" review pass:
- articles/index.tsx now passes an empty string, not a malformed
  "kind:pubkey:" address, when an article has no d tag. HighlightLayer
  treats a falsy address as "highlighting isn't available here."
- The selectionchange listener is only registered when both user and
  address are present (in the effect's deps), instead of always
  running selection tracking regardless of whether a highlight could
  ever be published.
- handleHighlight and the floating button both guard on address too,
  not just selection, so stale selection state from before a prop
  change went missing can't still trigger a publish.
- docs/apps.md corrected: the saved text comes from Selection.toString()
  (window.getSelection()), not Range.toString().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
This commit is contained in:
2026-09-06 18:27:13 +02:00
parent 3d77b0516c
commit 7c9eec8c82
3 changed files with 15 additions and 6 deletions

View File

@@ -105,8 +105,8 @@ export default function ExampleApp({ setTitle }: AppProps) {
`HighlightLayer` (`src/apps/articles/HighlightLayer.tsx`) tracks `window.getSelection()`
against the rendered article, not the raw markdown — the highlighted text saved to a kind
9802 event is whatever `Range.toString()` returns, i.e. the plain-text content the reader
actually saw, not markdown syntax.
9802 event is whatever that `Selection`'s `.toString()` returns, i.e. the plain-text content
the reader actually saw, not markdown syntax.
### Follow lists are a whole-list replacement

View File

@@ -34,6 +34,12 @@ export function HighlightLayer({
const { toast } = useToast();
useEffect(() => {
// No point tracking selection at all when highlighting can't happen —
// signed out, or the article has no usable address to attach one to.
// (Stale selection state from before either went missing is harmless:
// the button below also requires `user && address` to render.)
if (!user || !address) return;
function handleSelectionChange() {
const sel = window.getSelection();
const container = containerRef.current;
@@ -60,10 +66,10 @@ export function HighlightLayer({
document.addEventListener('selectionchange', handleSelectionChange);
return () => document.removeEventListener('selectionchange', handleSelectionChange);
}, []);
}, [user, address]);
const handleHighlight = async () => {
if (!selection) return;
if (!selection || !address) return;
const { text } = selection;
window.getSelection()?.removeAllRanges();
setSelection(null);
@@ -84,7 +90,7 @@ export function HighlightLayer({
<div ref={containerRef} className="relative">
{children}
{user && selection && (
{user && address && selection && (
<button
type="button"
// Selection collapses on mousedown before onClick fires unless

View File

@@ -321,7 +321,10 @@ function ArticleView({ event }: { event: NostrEvent }) {
</header>
<HighlightLayer
address={`${event.kind}:${event.pubkey}:${tagValue(event, 'd') ?? ''}`}
// An empty string (rather than a malformed "kind:pubkey:" address)
// when the article has no `d` tag — HighlightLayer treats a falsy
// address as "highlighting isn't available for this article."
address={tagValue(event, 'd') ? `${event.kind}:${event.pubkey}:${tagValue(event, 'd')}` : ''}
authorPubkey={event.pubkey}
>
<div className="mt-6 text-[15px]">