From 0d70e2dd9277a84a14151ef05f2aef29556e9832 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Jan 2026 14:28:19 +0000 Subject: [PATCH] feat: add post dialog with NostrEditor to user menu Add a "New Post" action to the user menu that opens a dialog with the new NostrEditor configured for composing kind 1 notes: - Full editor variant with 8 lines minimum - Gallery-style blob previews for media attachments - Button-only submit behavior (no keyboard shortcuts) - Profile and emoji autocomplete via suggestions - Blossom upload integration for media attachments The post action: - Creates kind 1 events using NoteBlueprint from applesauce-common - Adds emoji tags for custom emoji (NIP-30) - Adds imeta tags for media attachments (NIP-92) - Publishes via the global hub action runner --- src/components/PostDialog.tsx | 183 +++++++++++++++++++++++++++++ src/components/nostr/user-menu.tsx | 11 ++ 2 files changed, 194 insertions(+) create mode 100644 src/components/PostDialog.tsx diff --git a/src/components/PostDialog.tsx b/src/components/PostDialog.tsx new file mode 100644 index 0000000..f0cbfec --- /dev/null +++ b/src/components/PostDialog.tsx @@ -0,0 +1,183 @@ +import { useRef, useMemo, useState, useCallback } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { NostrEditor, type NostrEditorHandle } from "./editor/NostrEditor"; +import { createNostrSuggestions } from "./editor/suggestions"; +import { useProfileSearch } from "@/hooks/useProfileSearch"; +import { useEmojiSearch } from "@/hooks/useEmojiSearch"; +import { useBlossomUpload } from "@/hooks/useBlossomUpload"; +import { useAccount } from "@/hooks/useAccount"; +import { Loader2, Paperclip } from "lucide-react"; +import { toast } from "sonner"; +import { hub } from "@/services/hub"; +import { NoteBlueprint } from "applesauce-common/blueprints"; +import type { SerializedContent } from "./editor/types"; +import { lastValueFrom } from "rxjs"; +import type { ActionContext } from "applesauce-actions"; + +interface PostDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +// Action builder for creating a short text note +function CreateNoteAction(content: SerializedContent) { + return async ({ factory, sign, publish }: ActionContext) => { + // Build the note using NoteBlueprint + const draft = await factory.create(NoteBlueprint, content.text); + + // Add emoji tags if any custom emojis were used + for (const emoji of content.emojiTags) { + draft.tags.push(["emoji", emoji.shortcode, emoji.url]); + } + + // Add imeta tags for media attachments + for (const blob of content.blobAttachments) { + const imetaValues = [`url ${blob.url}`, `x ${blob.sha256}`]; + if (blob.mimeType) imetaValues.push(`m ${blob.mimeType}`); + if (blob.size) imetaValues.push(`size ${blob.size}`); + draft.tags.push(["imeta", ...imetaValues]); + } + + // Sign and publish the event + const event = await sign(draft); + await publish(event); + }; +} + +export default function PostDialog({ open, onOpenChange }: PostDialogProps) { + const { pubkey, canSign } = useAccount(); + const { searchProfiles } = useProfileSearch(); + const { searchEmojis } = useEmojiSearch(); + const editorRef = useRef(null); + const [isPublishing, setIsPublishing] = useState(false); + + // Blossom upload for attachments + const { open: openUpload, dialog: uploadDialog } = useBlossomUpload({ + accept: "image/*,video/*,audio/*", + onSuccess: (results) => { + if (results.length > 0 && editorRef.current) { + const { blob, server } = results[0]; + editorRef.current.insertBlob({ + url: blob.url, + sha256: blob.sha256, + mimeType: blob.type, + size: blob.size, + server, + }); + editorRef.current.focus(); + } + }, + }); + + // Create suggestions for the editor + const suggestions = useMemo( + () => + createNostrSuggestions({ + searchProfiles, + searchEmojis, + }), + [searchProfiles, searchEmojis], + ); + + // Handle publishing the post + const handlePublish = useCallback( + async (content: SerializedContent) => { + if (!canSign || !pubkey) { + toast.error("Please sign in to post"); + return; + } + + if (!content.text.trim()) { + toast.error("Please write something to post"); + return; + } + + setIsPublishing(true); + try { + // Execute the action (builds, signs, and publishes) + await lastValueFrom(hub.exec(CreateNoteAction, content)); + + toast.success("Post published!"); + editorRef.current?.clear(); + onOpenChange(false); + } catch (error) { + console.error("[PostDialog] Failed to publish:", error); + toast.error( + error instanceof Error ? error.message : "Failed to publish post", + ); + } finally { + setIsPublishing(false); + } + }, + [canSign, pubkey, onOpenChange], + ); + + // Handle submit button click + const handleSubmitClick = useCallback(() => { + if (editorRef.current) { + const content = editorRef.current.getSerializedContent(); + handlePublish(content); + } + }, [handlePublish]); + + return ( + <> + + + + New Post + + +
+ + +
+ + + +
+
+
+
+ + {uploadDialog} + + ); +} diff --git a/src/components/nostr/user-menu.tsx b/src/components/nostr/user-menu.tsx index 7bd0fec..62937f2 100644 --- a/src/components/nostr/user-menu.tsx +++ b/src/components/nostr/user-menu.tsx @@ -8,6 +8,7 @@ import { Eye, EyeOff, Zap, + PenSquare, } from "lucide-react"; import accounts from "@/services/accounts"; import { useProfile } from "@/hooks/useProfile"; @@ -42,6 +43,7 @@ import { RelayLink } from "./RelayLink"; import SettingsDialog from "@/components/SettingsDialog"; import LoginDialog from "./LoginDialog"; import ConnectWalletDialog from "@/components/ConnectWalletDialog"; +import PostDialog from "@/components/PostDialog"; import { useState } from "react"; import { useTheme } from "@/lib/themes"; import { toast } from "sonner"; @@ -93,6 +95,7 @@ export default function UserMenu() { const [showLogin, setShowLogin] = useState(false); const [showConnectWallet, setShowConnectWallet] = useState(false); const [showWalletInfo, setShowWalletInfo] = useState(false); + const [showPost, setShowPost] = useState(false); const { themeId, setTheme, availableThemes } = useTheme(); // Calculate monthly donations reactively from DB (last 30 days) @@ -218,6 +221,7 @@ export default function UserMenu() { onOpenChange={setShowConnectWallet} onConnected={openWallet} /> + {/* Wallet Info Dialog */} {nwcConnection && ( @@ -379,6 +383,13 @@ export default function UserMenu() { > + setShowPost(true)} + > + + New Post +