diff --git a/src/components/AddBookmarkModal.tsx b/src/components/AddBookmarkModal.tsx index 2e1af562..c251faae 100644 --- a/src/components/AddBookmarkModal.tsx +++ b/src/components/AddBookmarkModal.tsx @@ -5,13 +5,14 @@ import IconButton from './IconButton' interface AddBookmarkModalProps { onClose: () => void - onSave: (url: string, title?: string, description?: string) => Promise + onSave: (url: string, title?: string, description?: string, tags?: string[]) => Promise } const AddBookmarkModal: React.FC = ({ onClose, onSave }) => { const [url, setUrl] = useState('') const [title, setTitle] = useState('') const [description, setDescription] = useState('') + const [tagsInput, setTagsInput] = useState('') const [isSaving, setIsSaving] = useState(false) const [error, setError] = useState(null) @@ -34,10 +35,18 @@ const AddBookmarkModal: React.FC = ({ onClose, onSave }) try { setIsSaving(true) + + // Parse tags from comma-separated input + const tags = tagsInput + .split(',') + .map(tag => tag.trim()) + .filter(tag => tag.length > 0) + await onSave( url.trim(), title.trim() || undefined, - description.trim() || undefined + description.trim() || undefined, + tags.length > 0 ? tags : undefined ) onClose() } catch (err) { @@ -100,6 +109,21 @@ const AddBookmarkModal: React.FC = ({ onClose, onSave }) /> +
+ + setTagsInput(e.target.value)} + placeholder="comma, separated, tags" + disabled={isSaving} + /> +
+ Separate tags with commas (e.g., "nostr, web3, article") +
+
+ {error && (
{error}
)} diff --git a/src/components/SidebarHeader.tsx b/src/components/SidebarHeader.tsx index 395e6a28..bb5b568c 100644 --- a/src/components/SidebarHeader.tsx +++ b/src/components/SidebarHeader.tsx @@ -55,12 +55,12 @@ const SidebarHeader: React.FC = ({ onToggleCollapse, onLogou return `${activeAccount.pubkey.slice(0, 8)}...${activeAccount.pubkey.slice(-8)}` } - const handleSaveBookmark = async (url: string, title?: string, description?: string) => { + const handleSaveBookmark = async (url: string, title?: string, description?: string, tags?: string[]) => { if (!activeAccount || !relayPool) { throw new Error('Please login to create bookmarks') } - await createWebBookmark(url, title, description, activeAccount, relayPool, RELAYS) + await createWebBookmark(url, title, description, tags, activeAccount, relayPool, RELAYS) // Refresh bookmarks after creating if (onRefresh) { diff --git a/src/index.css b/src/index.css index 785b4ecf..597bb4ec 100644 --- a/src/index.css +++ b/src/index.css @@ -2280,6 +2280,13 @@ body { min-height: 80px; } +.form-helper-text { + margin-top: 0.25rem; + font-size: 0.8rem; + color: #999; + line-height: 1.4; +} + .modal-error { padding: 0.75rem; background: rgba(220, 53, 69, 0.1); diff --git a/src/services/webBookmarkService.ts b/src/services/webBookmarkService.ts index a7174e21..99611341 100644 --- a/src/services/webBookmarkService.ts +++ b/src/services/webBookmarkService.ts @@ -7,7 +7,8 @@ import { NostrEvent } from 'nostr-tools' * Creates a web bookmark event (NIP-B0, kind:39701) * @param url The URL to bookmark * @param title Optional title for the bookmark - * @param description Optional description + * @param description Optional description (goes in content field) + * @param bookmarkTags Optional array of tags/hashtags * @param account The user's account for signing * @param relayPool The relay pool for publishing * @param relays The relays to publish to @@ -17,6 +18,7 @@ export async function createWebBookmark( url: string, title: string | undefined, description: string | undefined, + bookmarkTags: string[] | undefined, account: IAccount, relayPool: RelayPool, relays: string[] @@ -25,34 +27,49 @@ export async function createWebBookmark( throw new Error('URL is required for web bookmark') } - // Validate URL format + // Validate URL format and extract the URL without scheme for d tag + let parsedUrl: URL try { - new URL(url) + parsedUrl = new URL(url) } catch { throw new Error('Invalid URL format') } + // d tag should be URL without scheme (as per NIP-B0) + const dTagValue = parsedUrl.host + parsedUrl.pathname + parsedUrl.search + parsedUrl.hash + const factory = new EventFactory({ signer: account }) + const now = Math.floor(Date.now() / 1000) // Build tags according to NIP-B0 const tags: string[][] = [ - ['d', url], // URL as identifier + ['d', dTagValue], // URL without scheme as identifier ] - if (title) { - tags.push(['title', title]) + // Add published_at tag (current timestamp) + tags.push(['published_at', now.toString()]) + + // Add title tag if provided + if (title && title.trim()) { + tags.push(['title', title.trim()]) } - if (description) { - tags.push(['summary', description]) + // Add t tags for each bookmark tag/hashtag + if (bookmarkTags && bookmarkTags.length > 0) { + bookmarkTags.forEach(tag => { + const trimmedTag = tag.trim() + if (trimmedTag) { + tags.push(['t', trimmedTag]) + } + }) } - // Create the event + // Create the event with description in content field (as per NIP-B0) const draft = await factory.create(async () => ({ kind: 39701, // NIP-B0 web bookmark - content: '', + content: description?.trim() || '', tags, - created_at: Math.floor(Date.now() / 1000) + created_at: now })) // Sign the event