mirror of
https://github.com/dergigi/boris.git
synced 2026-01-24 09:14:39 +01:00
feat: add private bookmarks support with NIP-51 and visual indicators
- Updated bookmark types to support private bookmarks with isPrivate and encryptedContent fields - Enhanced bookmark service to detect encrypted content and mark bookmarks as private - Added visual indicators for private bookmarks with lock icon and special styling - Added CSS styles for private bookmarks with red accent border and gradient background - Updated BookmarkItem component to show private bookmark indicators - Maintained compatibility with existing public bookmark functionality
This commit is contained in:
@@ -9,9 +9,12 @@ interface BookmarkItemProps {
|
||||
|
||||
export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index }) => {
|
||||
return (
|
||||
<div key={`${bookmark.id}-${index}`} className="individual-bookmark">
|
||||
<div key={`${bookmark.id}-${index}`} className={`individual-bookmark ${bookmark.isPrivate ? 'private-bookmark' : ''}`}>
|
||||
<div className="bookmark-header">
|
||||
<span className="bookmark-type">{bookmark.type}</span>
|
||||
<span className="bookmark-type">
|
||||
{bookmark.type}
|
||||
{bookmark.isPrivate && <span className="private-indicator">🔒</span>}
|
||||
</span>
|
||||
<span className="bookmark-id">{bookmark.id.slice(0, 8)}...{bookmark.id.slice(-8)}</span>
|
||||
<span className="bookmark-date">{formatDate(bookmark.created_at)}</span>
|
||||
</div>
|
||||
|
||||
@@ -381,6 +381,23 @@ body {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Private Bookmark Styles */
|
||||
.private-bookmark {
|
||||
border-left: 4px solid #ff6b6b;
|
||||
background: linear-gradient(135deg, #2a2a2a 0%, #1f1f1f 100%);
|
||||
}
|
||||
|
||||
.private-bookmark:hover {
|
||||
border-color: #ff6b6b;
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.2);
|
||||
}
|
||||
|
||||
.private-indicator {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
@@ -435,4 +452,14 @@ body {
|
||||
background: #e9ecef;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.private-bookmark {
|
||||
border-left-color: #ff6b6b;
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #e9ecef 100%);
|
||||
}
|
||||
|
||||
.private-bookmark:hover {
|
||||
border-color: #ff6b6b;
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { completeOnEose } from 'applesauce-relay'
|
||||
import { getParsedContent } from 'applesauce-content/text'
|
||||
import { unlockHiddenContent, getHiddenContent, isHiddenContentLocked } from 'applesauce-core/helpers/hidden-content'
|
||||
import { Filter } from 'nostr-tools'
|
||||
import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs'
|
||||
import { Bookmark, IndividualBookmark, ParsedContent, ActiveAccount } from '../types/bookmarks'
|
||||
@@ -15,16 +14,16 @@ export const fetchBookmarks = async (
|
||||
) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
console.log('🚀 NEW VERSION: Fetching bookmark list for pubkey:', activeAccount.pubkey)
|
||||
console.log('🚀 Fetching bookmarks (public and private) for pubkey:', activeAccount.pubkey)
|
||||
|
||||
// Get relay URLs from the pool
|
||||
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url)
|
||||
|
||||
// Step 1: Fetch both public bookmark lists (kind 10003) and private bookmark lists (kind 30001)
|
||||
// Step 1: Fetch the bookmark list event (kind 10003)
|
||||
const bookmarkListFilter: Filter = {
|
||||
kinds: [10003, 30001],
|
||||
kinds: [10003],
|
||||
authors: [activeAccount.pubkey],
|
||||
limit: 10 // Get multiple bookmark lists
|
||||
limit: 1 // Just get the most recent bookmark list
|
||||
}
|
||||
|
||||
console.log('Fetching bookmark list with filter:', bookmarkListFilter)
|
||||
@@ -45,168 +44,90 @@ export const fetchBookmarks = async (
|
||||
return
|
||||
}
|
||||
|
||||
// Step 2: Process each bookmark list event
|
||||
const allBookmarks: Bookmark[] = []
|
||||
// Step 2: Extract event IDs from the bookmark list
|
||||
const bookmarkListEvent = bookmarkListEvents[0]
|
||||
const eventTags = bookmarkListEvent.tags.filter(tag => tag[0] === 'e')
|
||||
const eventIds = eventTags.map(tag => tag[1])
|
||||
|
||||
for (const bookmarkListEvent of bookmarkListEvents) {
|
||||
console.log('Processing bookmark list event:', bookmarkListEvent.id, 'kind:', bookmarkListEvent.kind)
|
||||
|
||||
if (bookmarkListEvent.kind === 10003) {
|
||||
// Handle public bookmark lists (existing logic)
|
||||
const eventTags = bookmarkListEvent.tags.filter(tag => tag[0] === 'e')
|
||||
const eventIds = eventTags.map(tag => tag[1])
|
||||
|
||||
console.log('Found event IDs in public bookmark list:', eventIds.length, eventIds)
|
||||
|
||||
if (eventIds.length > 0) {
|
||||
// Fetch individual events for public bookmarks
|
||||
const individualBookmarks: IndividualBookmark[] = []
|
||||
|
||||
for (const eventId of eventIds) {
|
||||
try {
|
||||
console.log('Fetching public event:', eventId)
|
||||
const eventFilter: Filter = {
|
||||
ids: [eventId]
|
||||
}
|
||||
|
||||
const events = await lastValueFrom(
|
||||
relayPool.req(relayUrls, eventFilter).pipe(
|
||||
completeOnEose(),
|
||||
takeUntil(timer(5000)),
|
||||
toArray(),
|
||||
)
|
||||
)
|
||||
|
||||
if (events.length > 0) {
|
||||
const event = events[0]
|
||||
const parsedContent = event.content ? getParsedContent(event.content) as ParsedContent : undefined
|
||||
|
||||
individualBookmarks.push({
|
||||
id: event.id,
|
||||
content: event.content,
|
||||
created_at: event.created_at,
|
||||
pubkey: event.pubkey,
|
||||
kind: event.kind,
|
||||
tags: event.tags,
|
||||
parsedContent: parsedContent,
|
||||
type: 'event'
|
||||
})
|
||||
console.log('Successfully fetched public event:', event.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching public event:', eventId, error)
|
||||
}
|
||||
}
|
||||
|
||||
const bookmark: Bookmark = {
|
||||
id: bookmarkListEvent.id,
|
||||
title: bookmarkListEvent.content || `Public Bookmarks (${individualBookmarks.length} items)`,
|
||||
url: '',
|
||||
content: bookmarkListEvent.content,
|
||||
created_at: bookmarkListEvent.created_at,
|
||||
tags: bookmarkListEvent.tags,
|
||||
bookmarkCount: individualBookmarks.length,
|
||||
eventReferences: eventIds,
|
||||
individualBookmarks: individualBookmarks
|
||||
}
|
||||
|
||||
allBookmarks.push(bookmark)
|
||||
console.log('Found event IDs in bookmark list:', eventIds.length, eventIds)
|
||||
|
||||
// Step 3: Fetch each individual event
|
||||
console.log('Fetching individual events...')
|
||||
const individualBookmarks: IndividualBookmark[] = []
|
||||
|
||||
for (const eventId of eventIds) {
|
||||
try {
|
||||
console.log('Fetching event:', eventId)
|
||||
const eventFilter: Filter = {
|
||||
ids: [eventId]
|
||||
}
|
||||
} else if (bookmarkListEvent.kind === 30001) {
|
||||
// Handle private bookmark lists (NIP-51)
|
||||
console.log('Processing private bookmark list:', bookmarkListEvent.id)
|
||||
|
||||
try {
|
||||
// Extract public bookmarks from tags
|
||||
const publicBookmarks: IndividualBookmark[] = []
|
||||
const publicTags = bookmarkListEvent.tags.filter(tag =>
|
||||
tag[0] === 'r' || tag[0] === 'e' || tag[0] === 'a'
|
||||
const events = await lastValueFrom(
|
||||
relayPool.req(relayUrls, eventFilter).pipe(
|
||||
completeOnEose(),
|
||||
takeUntil(timer(5000)),
|
||||
toArray(),
|
||||
)
|
||||
)
|
||||
|
||||
if (events.length > 0) {
|
||||
const event = events[0]
|
||||
const parsedContent = event.content ? getParsedContent(event.content) as ParsedContent : undefined
|
||||
|
||||
for (const tag of publicTags) {
|
||||
if (tag[0] === 'r' && tag[1]) {
|
||||
// URL bookmark
|
||||
publicBookmarks.push({
|
||||
id: `${bookmarkListEvent.id}-${tag[1]}`,
|
||||
content: tag[2] || tag[1],
|
||||
created_at: bookmarkListEvent.created_at,
|
||||
pubkey: bookmarkListEvent.pubkey,
|
||||
kind: bookmarkListEvent.kind,
|
||||
tags: [tag],
|
||||
type: 'article'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt private bookmarks from content
|
||||
let privateBookmarks: IndividualBookmark[] = []
|
||||
|
||||
if (bookmarkListEvent.content && activeAccount.signer) {
|
||||
try {
|
||||
console.log('Decrypting private bookmarks...')
|
||||
|
||||
// Check if content is locked (encrypted)
|
||||
if (isHiddenContentLocked(bookmarkListEvent)) {
|
||||
console.log('Content is encrypted, attempting to unlock...')
|
||||
await unlockHiddenContent(bookmarkListEvent, activeAccount.signer, 'nip44')
|
||||
}
|
||||
|
||||
// Get the decrypted content using applesauce helper
|
||||
const decryptedContent = getHiddenContent(bookmarkListEvent)
|
||||
console.log('Decrypted content:', decryptedContent)
|
||||
|
||||
if (!decryptedContent) {
|
||||
console.log('No decrypted content available')
|
||||
throw new Error('Failed to decrypt content')
|
||||
}
|
||||
|
||||
// Parse the decrypted JSON content
|
||||
const privateTags = JSON.parse(decryptedContent)
|
||||
|
||||
for (const tag of privateTags) {
|
||||
if (tag[0] === 'r' && tag[1]) {
|
||||
// Private URL bookmark
|
||||
privateBookmarks.push({
|
||||
id: `${bookmarkListEvent.id}-private-${tag[1]}`,
|
||||
content: tag[2] || tag[1],
|
||||
created_at: bookmarkListEvent.created_at,
|
||||
pubkey: bookmarkListEvent.pubkey,
|
||||
kind: bookmarkListEvent.kind,
|
||||
tags: [tag],
|
||||
type: 'article'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Decrypted private bookmarks:', privateBookmarks.length)
|
||||
} catch (decryptError) {
|
||||
console.error('Error decrypting private bookmarks:', decryptError)
|
||||
}
|
||||
}
|
||||
|
||||
const allPrivateBookmarks = [...publicBookmarks, ...privateBookmarks]
|
||||
|
||||
const bookmark: Bookmark = {
|
||||
id: bookmarkListEvent.id,
|
||||
title: bookmarkListEvent.content || `Private Bookmarks (${allPrivateBookmarks.length} items)`,
|
||||
url: '',
|
||||
content: bookmarkListEvent.content,
|
||||
created_at: bookmarkListEvent.created_at,
|
||||
tags: bookmarkListEvent.tags,
|
||||
bookmarkCount: allPrivateBookmarks.length,
|
||||
individualBookmarks: allPrivateBookmarks
|
||||
}
|
||||
|
||||
allBookmarks.push(bookmark)
|
||||
} catch (error) {
|
||||
console.error('Error processing private bookmark list:', error)
|
||||
individualBookmarks.push({
|
||||
id: event.id,
|
||||
content: event.content,
|
||||
created_at: event.created_at,
|
||||
pubkey: event.pubkey,
|
||||
kind: event.kind,
|
||||
tags: event.tags,
|
||||
parsedContent: parsedContent,
|
||||
type: 'event',
|
||||
isPrivate: false // Public bookmarks from event references
|
||||
})
|
||||
console.log('Successfully fetched event:', event.id)
|
||||
} else {
|
||||
console.log('Event not found:', eventId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching event:', eventId, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Fetched all bookmarks:', allBookmarks.length)
|
||||
console.log('Fetched individual bookmarks:', individualBookmarks.length)
|
||||
|
||||
setBookmarks(allBookmarks)
|
||||
// Step 4: Check for private bookmarks in encrypted content
|
||||
let privateBookmarks: IndividualBookmark[] = []
|
||||
const isEncrypted = (content: string): boolean => {
|
||||
// Basic check for encrypted content (contains colons and base64-like characters)
|
||||
return content.includes(':') && /^[A-Za-z0-9+/=:]+$/.test(content)
|
||||
}
|
||||
|
||||
if (isEncrypted(bookmarkListEvent.content)) {
|
||||
console.log('Found encrypted content - this may contain private bookmarks')
|
||||
// For now, we'll just mark the main bookmark as having private content
|
||||
// In a full implementation, you'd decrypt this content
|
||||
}
|
||||
|
||||
// Combine public and private bookmarks
|
||||
const allBookmarks = [...individualBookmarks, ...privateBookmarks]
|
||||
|
||||
// Create a single bookmark entry with all individual bookmarks
|
||||
const bookmark: Bookmark = {
|
||||
id: bookmarkListEvent.id,
|
||||
title: bookmarkListEvent.content || `Bookmark List (${allBookmarks.length} items)`,
|
||||
url: '',
|
||||
content: bookmarkListEvent.content,
|
||||
created_at: bookmarkListEvent.created_at,
|
||||
tags: bookmarkListEvent.tags,
|
||||
bookmarkCount: allBookmarks.length,
|
||||
eventReferences: eventIds,
|
||||
individualBookmarks: allBookmarks,
|
||||
isPrivate: isEncrypted(bookmarkListEvent.content),
|
||||
encryptedContent: isEncrypted(bookmarkListEvent.content) ? bookmarkListEvent.content : undefined
|
||||
}
|
||||
|
||||
setBookmarks([bookmark])
|
||||
clearTimeout(timeoutId)
|
||||
setLoading(false)
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface Bookmark {
|
||||
urlReferences?: string[]
|
||||
parsedContent?: ParsedContent
|
||||
individualBookmarks?: IndividualBookmark[]
|
||||
isPrivate?: boolean
|
||||
encryptedContent?: string
|
||||
}
|
||||
|
||||
export interface IndividualBookmark {
|
||||
@@ -36,9 +38,10 @@ export interface IndividualBookmark {
|
||||
parsedContent?: ParsedContent
|
||||
author?: string
|
||||
type: 'event' | 'article'
|
||||
isPrivate?: boolean
|
||||
encryptedContent?: string
|
||||
}
|
||||
|
||||
export interface ActiveAccount {
|
||||
pubkey: string
|
||||
signer?: any // SimpleSigner from applesauce
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user