mirror of
https://github.com/dergigi/boris.git
synced 2026-01-07 00:44:52 +01:00
refactor: remove deprecated bookmark service files
Deleted bookmarkService.ts and bookmarkStream.ts: - All functionality now consolidated in bookmarkController.ts - No more duplication of streaming/decrypt logic - Single source of truth for bookmark loading
This commit is contained in:
@@ -1,191 +0,0 @@
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import {
|
||||
AccountWithExtension,
|
||||
NostrEvent,
|
||||
hydrateItems,
|
||||
isAccountWithExtension,
|
||||
dedupeBookmarksById,
|
||||
extractUrlsFromContent
|
||||
} from './bookmarkHelpers'
|
||||
import { Bookmark } from '../types/bookmarks'
|
||||
import { collectBookmarksFromEvents } from './bookmarkProcessing.ts'
|
||||
import { UserSettings } from './settingsService'
|
||||
import { rebroadcastEvents } from './rebroadcastService'
|
||||
import { queryEvents } from './dataFetch'
|
||||
import { loadBookmarksStream } from './bookmarkStream'
|
||||
|
||||
export const fetchBookmarks = async (
|
||||
relayPool: RelayPool,
|
||||
activeAccount: unknown,
|
||||
accountManager: { getActive: () => unknown },
|
||||
setBookmarks: (bookmarks: Bookmark[]) => void,
|
||||
settings?: UserSettings,
|
||||
onProgressUpdate?: () => void
|
||||
) => {
|
||||
try {
|
||||
if (!isAccountWithExtension(activeAccount)) {
|
||||
throw new Error('Invalid account object provided')
|
||||
}
|
||||
|
||||
// Get signer for bookmark processing
|
||||
const maybeAccount = activeAccount as AccountWithExtension
|
||||
let signerCandidate: unknown = maybeAccount
|
||||
const hasNip04Prop = (signerCandidate as { nip04?: unknown })?.nip04 !== undefined
|
||||
const hasNip44Prop = (signerCandidate as { nip44?: unknown })?.nip44 !== undefined
|
||||
if (signerCandidate && !hasNip04Prop && !hasNip44Prop && maybeAccount?.signer) {
|
||||
signerCandidate = maybeAccount.signer
|
||||
}
|
||||
|
||||
// Helper to build and update bookmark from current events
|
||||
const updateBookmarks = async (events: NostrEvent[]) => {
|
||||
if (events.length === 0) return
|
||||
|
||||
// Collect bookmarks from all events
|
||||
const { publicItemsAll, privateItemsAll, newestCreatedAt, latestContent, allTags } =
|
||||
await collectBookmarksFromEvents(events, activeAccount, signerCandidate)
|
||||
|
||||
const allItems = [...publicItemsAll, ...privateItemsAll]
|
||||
|
||||
// Separate hex IDs from coordinates
|
||||
const noteIds: string[] = []
|
||||
const coordinates: string[] = []
|
||||
|
||||
allItems.forEach(i => {
|
||||
if (/^[0-9a-f]{64}$/i.test(i.id)) {
|
||||
noteIds.push(i.id)
|
||||
} else if (i.id.includes(':')) {
|
||||
coordinates.push(i.id)
|
||||
}
|
||||
})
|
||||
|
||||
const idToEvent: Map<string, NostrEvent> = new Map()
|
||||
|
||||
// Fetch regular events by ID
|
||||
if (noteIds.length > 0) {
|
||||
try {
|
||||
const fetchedEvents = await queryEvents(
|
||||
relayPool,
|
||||
{ ids: Array.from(new Set(noteIds)) },
|
||||
{}
|
||||
)
|
||||
fetchedEvents.forEach((e: NostrEvent) => {
|
||||
idToEvent.set(e.id, e)
|
||||
if (e.kind && e.kind >= 30000 && e.kind < 40000) {
|
||||
const dTag = e.tags?.find((t: string[]) => t[0] === 'd')?.[1] || ''
|
||||
const coordinate = `${e.kind}:${e.pubkey}:${dTag}`
|
||||
idToEvent.set(coordinate, e)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch events by ID:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch addressable events by coordinates
|
||||
if (coordinates.length > 0) {
|
||||
try {
|
||||
const byKind = new Map<number, Array<{ pubkey: string; identifier: string }>>()
|
||||
|
||||
coordinates.forEach(coord => {
|
||||
const parts = coord.split(':')
|
||||
const kind = parseInt(parts[0])
|
||||
const pubkey = parts[1]
|
||||
const identifier = parts[2] || ''
|
||||
|
||||
if (!byKind.has(kind)) {
|
||||
byKind.set(kind, [])
|
||||
}
|
||||
byKind.get(kind)!.push({ pubkey, identifier })
|
||||
})
|
||||
|
||||
for (const [kind, items] of byKind.entries()) {
|
||||
const authors = Array.from(new Set(items.map(i => i.pubkey)))
|
||||
const identifiers = Array.from(new Set(items.map(i => i.identifier)))
|
||||
|
||||
const fetchedEvents = await queryEvents(
|
||||
relayPool,
|
||||
{ kinds: [kind], authors, '#d': identifiers },
|
||||
{}
|
||||
)
|
||||
|
||||
fetchedEvents.forEach((e: NostrEvent) => {
|
||||
const dTag = e.tags?.find((t: string[]) => t[0] === 'd')?.[1] || ''
|
||||
const coordinate = `${e.kind}:${e.pubkey}:${dTag}`
|
||||
idToEvent.set(coordinate, e)
|
||||
idToEvent.set(e.id, e)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch addressable events:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const allBookmarks = dedupeBookmarksById([
|
||||
...hydrateItems(publicItemsAll, idToEvent),
|
||||
...hydrateItems(privateItemsAll, idToEvent)
|
||||
])
|
||||
|
||||
const enriched = allBookmarks.map(b => ({
|
||||
...b,
|
||||
tags: b.tags || [],
|
||||
content: b.content || ''
|
||||
}))
|
||||
const sortedBookmarks = enriched
|
||||
.map(b => ({ ...b, urlReferences: extractUrlsFromContent(b.content) }))
|
||||
.sort((a, b) => ((b.added_at || 0) - (a.added_at || 0)) || ((b.created_at || 0) - (a.created_at || 0)))
|
||||
|
||||
const bookmark: Bookmark = {
|
||||
id: `${activeAccount.pubkey}-bookmarks`,
|
||||
title: `Bookmarks (${sortedBookmarks.length})`,
|
||||
url: '',
|
||||
content: latestContent,
|
||||
created_at: newestCreatedAt || Math.floor(Date.now() / 1000),
|
||||
tags: allTags,
|
||||
bookmarkCount: sortedBookmarks.length,
|
||||
eventReferences: allTags.filter((tag: string[]) => tag[0] === 'e').map((tag: string[]) => tag[1]),
|
||||
individualBookmarks: sortedBookmarks,
|
||||
isPrivate: privateItemsAll.length > 0,
|
||||
encryptedContent: undefined
|
||||
}
|
||||
|
||||
setBookmarks([bookmark])
|
||||
}
|
||||
|
||||
// Use shared streaming helper for consistent behavior with Debug page
|
||||
// Progressive updates via callbacks (non-blocking)
|
||||
const { events: dedupedEvents } = await loadBookmarksStream({
|
||||
relayPool,
|
||||
activeAccount: maybeAccount,
|
||||
accountManager,
|
||||
onEvent: () => {
|
||||
// Signal that an event arrived (for loading indicator updates)
|
||||
if (onProgressUpdate) {
|
||||
onProgressUpdate()
|
||||
}
|
||||
},
|
||||
onDecryptComplete: () => {
|
||||
// Signal that a decrypt completed (for loading indicator updates)
|
||||
if (onProgressUpdate) {
|
||||
onProgressUpdate()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Rebroadcast bookmark events to local/all relays based on settings
|
||||
await rebroadcastEvents(dedupedEvents, relayPool, settings)
|
||||
|
||||
if (dedupedEvents.length === 0) {
|
||||
console.log('[app] ⚠️ No bookmark events found')
|
||||
setBookmarks([])
|
||||
return
|
||||
}
|
||||
|
||||
// Final update with all events (now with decrypted content)
|
||||
console.log('[app] 🔄 Final bookmark processing with', dedupedEvents.length, 'events')
|
||||
await updateBookmarks(dedupedEvents)
|
||||
console.log('[app] ✅ Bookmarks processing complete')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch bookmarks:', error)
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { Helpers } from 'applesauce-core'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
import { queryEvents } from './dataFetch'
|
||||
import { KINDS } from '../config/kinds'
|
||||
import { collectBookmarksFromEvents } from './bookmarkProcessing'
|
||||
|
||||
/**
|
||||
* Get unique key for event deduplication
|
||||
* Replaceable events (30001, 30003) use kind:pubkey:dtag
|
||||
* Simple lists (10003) use kind:pubkey
|
||||
* Web bookmarks (39701) use event id
|
||||
*/
|
||||
export function getEventKey(evt: NostrEvent): string {
|
||||
if (evt.kind === 30003 || evt.kind === 30001) {
|
||||
// Replaceable: kind:pubkey:dtag
|
||||
const dTag = evt.tags?.find((t: string[]) => t[0] === 'd')?.[1] || ''
|
||||
return `${evt.kind}:${evt.pubkey}:${dTag}`
|
||||
} else if (evt.kind === 10003) {
|
||||
// Simple list: kind:pubkey
|
||||
return `${evt.kind}:${evt.pubkey}`
|
||||
}
|
||||
// Web bookmarks: use event id (no deduplication)
|
||||
return evt.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event has encrypted content
|
||||
* Detects NIP-44 (via Helpers), NIP-04 (?iv= suffix), and encrypted tags
|
||||
*/
|
||||
export function hasEncryptedContent(evt: NostrEvent): boolean {
|
||||
// Check for NIP-44 encrypted content (detected by Helpers)
|
||||
if (Helpers.hasHiddenContent(evt)) return true
|
||||
|
||||
// Check for NIP-04 encrypted content (base64 with ?iv= suffix)
|
||||
if (evt.content && evt.content.includes('?iv=')) return true
|
||||
|
||||
// Check for encrypted tags
|
||||
if (Helpers.hasHiddenTags(evt) && !Helpers.isHiddenTagsUnlocked(evt)) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
interface LoadBookmarksStreamOptions {
|
||||
relayPool: RelayPool
|
||||
activeAccount: { pubkey: string; [key: string]: unknown }
|
||||
accountManager: { getActive: () => unknown }
|
||||
onEvent?: (event: NostrEvent) => void
|
||||
onDecryptStart?: (eventId: string) => void
|
||||
onDecryptComplete?: (eventId: string, success: boolean) => void
|
||||
}
|
||||
|
||||
interface LoadBookmarksStreamResult {
|
||||
events: NostrEvent[]
|
||||
decryptedCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Load bookmark events with streaming and non-blocking decryption
|
||||
* - Streams events via onEvent callback as they arrive
|
||||
* - Deduplicates by getEventKey
|
||||
* - Decrypts encrypted events AFTER query completes (non-blocking UI)
|
||||
* - Trusts EOSE signal to complete
|
||||
*/
|
||||
export async function loadBookmarksStream(
|
||||
options: LoadBookmarksStreamOptions
|
||||
): Promise<LoadBookmarksStreamResult> {
|
||||
const {
|
||||
relayPool,
|
||||
activeAccount,
|
||||
accountManager,
|
||||
onEvent,
|
||||
onDecryptStart,
|
||||
onDecryptComplete
|
||||
} = options
|
||||
|
||||
console.log('[app] 🔍 Fetching bookmark events with streaming')
|
||||
console.log('[app] Account:', activeAccount.pubkey.slice(0, 8))
|
||||
|
||||
// Track events with deduplication as they arrive
|
||||
const eventMap = new Map<string, NostrEvent>()
|
||||
let processedCount = 0
|
||||
|
||||
// Get signer for auto-decryption
|
||||
const fullAccount = accountManager.getActive() as {
|
||||
pubkey: string
|
||||
signer?: unknown
|
||||
nip04?: unknown
|
||||
nip44?: unknown
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
const maybeAccount = fullAccount || activeAccount
|
||||
let signerCandidate: unknown = maybeAccount
|
||||
const hasNip04Prop = (signerCandidate as { nip04?: unknown })?.nip04 !== undefined
|
||||
const hasNip44Prop = (signerCandidate as { nip44?: unknown })?.nip44 !== undefined
|
||||
if (signerCandidate && !hasNip04Prop && !hasNip44Prop && maybeAccount?.signer) {
|
||||
signerCandidate = maybeAccount.signer
|
||||
}
|
||||
|
||||
// Stream events (just collect, decrypt after)
|
||||
const rawEvents = await queryEvents(
|
||||
relayPool,
|
||||
{ kinds: [KINDS.ListSimple, KINDS.ListReplaceable, KINDS.List, KINDS.WebBookmark], authors: [activeAccount.pubkey] },
|
||||
{
|
||||
onEvent: (evt) => {
|
||||
// Deduplicate by key
|
||||
const key = getEventKey(evt)
|
||||
const existing = eventMap.get(key)
|
||||
|
||||
if (existing && (existing.created_at || 0) >= (evt.created_at || 0)) {
|
||||
return // Keep existing (it's newer or same)
|
||||
}
|
||||
|
||||
// Add/update event
|
||||
eventMap.set(key, evt)
|
||||
processedCount++
|
||||
|
||||
console.log(`[app] 📨 Event ${processedCount}: kind=${evt.kind}, id=${evt.id.slice(0, 8)}, hasEncrypted=${hasEncryptedContent(evt)}`)
|
||||
|
||||
// Call optional callback for progressive UI updates
|
||||
if (onEvent) {
|
||||
onEvent(evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
console.log('[app] 📊 Query complete, raw events fetched:', rawEvents.length, 'events')
|
||||
|
||||
const dedupedEvents = Array.from(eventMap.values())
|
||||
console.log('[app] 📋 After deduplication:', dedupedEvents.length, 'bookmark events')
|
||||
|
||||
if (dedupedEvents.length === 0) {
|
||||
console.log('[app] ⚠️ No bookmark events found')
|
||||
return { events: [], decryptedCount: 0 }
|
||||
}
|
||||
|
||||
// Auto-decrypt events with encrypted content (batch processing after EOSE)
|
||||
const encryptedEvents = dedupedEvents.filter(evt => hasEncryptedContent(evt))
|
||||
let decryptedCount = 0
|
||||
|
||||
if (encryptedEvents.length > 0) {
|
||||
console.log('[app] 🔓 Auto-decrypting', encryptedEvents.length, 'encrypted events')
|
||||
for (const evt of encryptedEvents) {
|
||||
try {
|
||||
if (onDecryptStart) {
|
||||
onDecryptStart(evt.id)
|
||||
}
|
||||
|
||||
// Trigger decryption - this unlocks the content for the bookmark collection
|
||||
await collectBookmarksFromEvents([evt], activeAccount, signerCandidate)
|
||||
decryptedCount++
|
||||
console.log('[app] ✅ Auto-decrypted:', evt.id.slice(0, 8))
|
||||
|
||||
if (onDecryptComplete) {
|
||||
onDecryptComplete(evt.id, true)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[app] ❌ Auto-decrypt failed:', evt.id.slice(0, 8), error)
|
||||
if (onDecryptComplete) {
|
||||
onDecryptComplete(evt.id, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[app] ✅ Bookmark streaming complete:', dedupedEvents.length, 'events,', decryptedCount, 'decrypted')
|
||||
|
||||
return { events: dedupedEvents, decryptedCount }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user