mirror of
https://github.com/dergigi/boris.git
synced 2026-01-28 11:14:22 +01:00
feat: add tags support to web bookmarks per NIP-B0
- Added tags input field to bookmark modal (comma-separated) - Updated createWebBookmark to accept tags array - Tags are added as 't' tags per NIP-B0 spec - Added published_at tag with current timestamp - Moved description to content field (per spec, not summary tag) - d tag now uses URL without scheme (host + path + search + hash) - Added helper text to explain tag formatting - Styled form-helper-text for better UX
This commit is contained in:
@@ -5,13 +5,14 @@ import IconButton from './IconButton'
|
||||
|
||||
interface AddBookmarkModalProps {
|
||||
onClose: () => void
|
||||
onSave: (url: string, title?: string, description?: string) => Promise<void>
|
||||
onSave: (url: string, title?: string, description?: string, tags?: string[]) => Promise<void>
|
||||
}
|
||||
|
||||
const AddBookmarkModal: React.FC<AddBookmarkModalProps> = ({ 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<string | null>(null)
|
||||
|
||||
@@ -34,10 +35,18 @@ const AddBookmarkModal: React.FC<AddBookmarkModalProps> = ({ 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<AddBookmarkModalProps> = ({ onClose, onSave })
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="bookmark-tags">Tags</label>
|
||||
<input
|
||||
id="bookmark-tags"
|
||||
type="text"
|
||||
value={tagsInput}
|
||||
onChange={(e) => setTagsInput(e.target.value)}
|
||||
placeholder="comma, separated, tags"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<div className="form-helper-text">
|
||||
Separate tags with commas (e.g., "nostr, web3, article")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="modal-error">{error}</div>
|
||||
)}
|
||||
|
||||
@@ -55,12 +55,12 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ 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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user