Files
website/src/apps/notes/Draft.tsx
highperfocused 1e4051f15a 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
2026-09-06 17:56:26 +02:00

108 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { Loader2, Send, Trash2 } from 'lucide-react';
import { AppBody, AppLayout, AppToolbar } from '@/components/os/AppChrome';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { useNostrPublish } from '@/hooks/useNostrPublish';
import { useToast } from '@/hooks/useToast';
const DRAFT_KEY = 'layer-os:draft-note';
/**
* A blank note kept as a local draft — not published until you say so, and
* not lost between sessions or windows in the meantime. This is the "open a
* new note" entry point (reachable from the Go menu, the command palette,
* and the Feed toolbar) for writing something before deciding it is worth
* publishing.
*/
export function DraftNote({ onPublished }: { onPublished: (id: string) => void }) {
const { user } = useCurrentUser();
const [draft, setDraft] = useLocalStorage(DRAFT_KEY, '');
const publish = useNostrPublish();
const { toast } = useToast();
const [confirmingDiscard, setConfirmingDiscard] = useState(false);
const trimmed = draft.trim();
const handlePublish = async () => {
// Guarded here too, not just via the button's `disabled` — React hasn't
// necessarily re-rendered with publish.isPending yet when a second click
// lands in the same tick, and mutateAsync itself doesn't dedupe calls.
if (!trimmed || publish.isPending) return;
try {
const event = await publish.mutateAsync({ kind: 1, content: trimmed, tags: [] });
setDraft('');
toast({ title: 'Note published' });
onPublished(event.id);
} catch (error) {
toast({
title: 'Could not publish',
description: error instanceof Error ? error.message : 'No relay accepted the note.',
variant: 'destructive',
});
}
};
const handleDiscard = () => {
if (!confirmingDiscard) {
setConfirmingDiscard(true);
return;
}
setDraft('');
setConfirmingDiscard(false);
};
return (
<AppLayout>
<AppToolbar>
<span className="text-[13px] font-medium">New Note</span>
<div className="ml-auto flex items-center gap-2">
{draft && (
<Button
variant="ghost"
size="sm"
className="h-7 gap-1.5 px-2 text-xs text-muted-foreground"
onClick={handleDiscard}
onBlur={() => setConfirmingDiscard(false)}
>
<Trash2 className="size-3.5" aria-hidden />
{confirmingDiscard ? 'Click again to discard' : 'Discard draft'}
</Button>
)}
{user && (
<Button
size="sm"
className="h-7 gap-1.5 px-2.5 text-xs"
onClick={handlePublish}
disabled={!trimmed || publish.isPending}
>
{publish.isPending ? (
<Loader2 className="size-3.5 animate-spin" aria-hidden />
) : (
<Send className="size-3.5" aria-hidden />
)}
Publish
</Button>
)}
</div>
</AppToolbar>
<AppBody className="flex flex-col p-4">
<Textarea
value={draft}
onChange={(event) => setDraft(event.target.value)}
placeholder={
user
? 'Write something… its kept as a local draft until you publish it.'
: 'Write something… its kept as a local draft on this device. Sign in to publish it.'
}
autoFocus
className="min-h-40 flex-1 resize-none border-0 bg-transparent p-0 text-[15px] leading-relaxed shadow-none focus-visible:ring-0"
/>
</AppBody>
</AppLayout>
);
}