diff --git a/.cursor/rules/web-bookmarks.mdc b/.cursor/rules/web-bookmarks.mdc new file mode 100644 index 00000000..b096362a --- /dev/null +++ b/.cursor/rules/web-bookmarks.mdc @@ -0,0 +1,10 @@ +--- +description: anything to do with "web bookmarks" aka NIP-B0 +alwaysApply: false +--- + +The app also supports web bookmarks (`kind:39701`) which are distinct from public/private bookmarks as defined in NIP-51. + +See NIP-B0 for details: + +- https://github.com/nostr-protocol/nips/blob/master/B0.md diff --git a/src/components/BookmarkViews/CardView.tsx b/src/components/BookmarkViews/CardView.tsx index 7e007117..1f0eb1e8 100644 --- a/src/components/BookmarkViews/CardView.tsx +++ b/src/components/BookmarkViews/CardView.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faBookmark, faUserLock, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons' +import { faBookmark, faUserLock, faChevronDown, faChevronUp, faGlobe } from '@fortawesome/free-solid-svg-icons' import { IndividualBookmark } from '../../types/bookmarks' import { formatDate, renderParsedContent } from '../../utils/bookmarkUtils' import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles' @@ -42,6 +42,7 @@ export const CardView: React.FC = ({ const contentLength = (bookmark.content || '').length const shouldTruncate = !expanded && contentLength > 210 const isArticle = bookmark.kind === 30023 + const isWebBookmark = bookmark.kind === 39701 return (
@@ -54,7 +55,12 @@ export const CardView: React.FC = ({ )}
- {bookmark.isPrivate ? ( + {isWebBookmark ? ( + + + + + ) : bookmark.isPrivate ? ( <> diff --git a/src/components/BookmarkViews/CompactView.tsx b/src/components/BookmarkViews/CompactView.tsx index 83961ab4..8a164e0b 100644 --- a/src/components/BookmarkViews/CompactView.tsx +++ b/src/components/BookmarkViews/CompactView.tsx @@ -1,6 +1,6 @@ import React from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faBookmark, faUserLock } from '@fortawesome/free-solid-svg-icons' +import { faBookmark, faUserLock, faGlobe } from '@fortawesome/free-solid-svg-icons' import { IndividualBookmark } from '../../types/bookmarks' import { formatDate } from '../../utils/bookmarkUtils' import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles' @@ -27,7 +27,8 @@ export const CompactView: React.FC = ({ firstUrlClassification }) => { const isArticle = bookmark.kind === 30023 - const isClickable = hasUrls || isArticle + const isWebBookmark = bookmark.kind === 39701 + const isClickable = hasUrls || isArticle || isWebBookmark const handleCompactClick = () => { if (!onSelectUrl) return @@ -48,7 +49,12 @@ export const CompactView: React.FC = ({ tabIndex={isClickable ? 0 : undefined} > - {bookmark.isPrivate ? ( + {isWebBookmark ? ( + + + + + ) : bookmark.isPrivate ? ( <> diff --git a/src/services/bookmarkEvents.ts b/src/services/bookmarkEvents.ts index 002b3faf..88bade3d 100644 --- a/src/services/bookmarkEvents.ts +++ b/src/services/bookmarkEvents.ts @@ -15,6 +15,9 @@ export function dedupeNip51Events(events: NostrEvent[]): NostrEvent[] { } const unique = Array.from(byId.values()) + // Separate web bookmarks (kind:39701) from list-based bookmarks + const webBookmarks = unique.filter(e => e.kind === 39701) + const bookmarkLists = unique .filter(e => e.kind === 10003 || e.kind === 30001) .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) @@ -33,6 +36,8 @@ export function dedupeNip51Events(events: NostrEvent[]): NostrEvent[] { const out: NostrEvent[] = [] if (latestBookmarkList) out.push(latestBookmarkList) out.push(...setsAndNamedLists) + // Add web bookmarks as individual events + out.push(...webBookmarks) return out } diff --git a/src/services/bookmarkProcessing.ts b/src/services/bookmarkProcessing.ts index 76c5451c..54cb87f1 100644 --- a/src/services/bookmarkProcessing.ts +++ b/src/services/bookmarkProcessing.ts @@ -33,6 +33,23 @@ export async function collectBookmarksFromEvents( if (!latestContent && evt.content && !Helpers.hasHiddenContent(evt)) latestContent = evt.content if (Array.isArray(evt.tags)) allTags = allTags.concat(evt.tags) + // Handle web bookmarks (kind:39701) as individual bookmarks + if (evt.kind === 39701) { + publicItemsAll.push({ + id: evt.id, + content: evt.content || '', + created_at: evt.created_at || Math.floor(Date.now() / 1000), + pubkey: evt.pubkey, + kind: evt.kind, + tags: evt.tags || [], + parsedContent: undefined, + type: 'web' as const, + isPrivate: false, + added_at: evt.created_at || Math.floor(Date.now() / 1000) + }) + continue + } + const pub = Helpers.getPublicBookmarks(evt) publicItemsAll.push(...processApplesauceBookmarks(pub, activeAccount, false)) diff --git a/src/services/bookmarkService.ts b/src/services/bookmarkService.ts index f22f6659..9c1d9396 100644 --- a/src/services/bookmarkService.ts +++ b/src/services/bookmarkService.ts @@ -29,11 +29,11 @@ export const fetchBookmarks = async ( } // Get relay URLs from the pool const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url) - // Fetch bookmark events - NIP-51 standards and legacy formats + // Fetch bookmark events - NIP-51 standards, legacy formats, and web bookmarks (NIP-B0) console.log('🔍 Fetching bookmark events from relays:', relayUrls) const rawEvents = await lastValueFrom( relayPool - .req(relayUrls, { kinds: [10003, 30003, 30001], authors: [activeAccount.pubkey] }) + .req(relayUrls, { kinds: [10003, 30003, 30001, 39701], authors: [activeAccount.pubkey] }) .pipe(completeOnEose(), takeUntil(timer(20000)), toArray()) ) console.log('📊 Raw events fetched:', rawEvents.length, 'events') diff --git a/src/types/bookmarks.ts b/src/types/bookmarks.ts index 49e3b1bc..24cb3d62 100644 --- a/src/types/bookmarks.ts +++ b/src/types/bookmarks.ts @@ -37,7 +37,7 @@ export interface IndividualBookmark { tags: string[][] parsedContent?: ParsedContent author?: string - type: 'event' | 'article' + type: 'event' | 'article' | 'web' isPrivate?: boolean encryptedContent?: string // When the item was added to the bookmark list (synthetic, for sorting)