mirror of
https://github.com/dergigi/boris.git
synced 2026-01-01 22:14:20 +01:00
feat: create shared bookmark controller for Debug-driven loading
Created bookmarkController.ts singleton: - Encapsulates Debug's working streaming/decrypt logic - API: start(), onRawEvent(), onBookmarks(), onLoading(), reset() - Live deduplication, sequential decrypt, progressive updates Updated App.tsx: - Removed automatic loading triggers (useEffect) - Subscribe to controller for bookmarks/loading state - Manual refresh calls controller.start() Updated Debug.tsx: - Uses controller.start() instead of internal loader - Subscribes to onRawEvent for UI display (unchanged) - Pressing 'Load Bookmarks' now populates app sidebar No automatic loads on login/mount. App passively receives updates from Debug-driven controller.
This commit is contained in:
71
src/App.tsx
71
src/App.tsx
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
|
||||
@@ -20,7 +20,7 @@ import { RELAYS } from './config/relays'
|
||||
import { SkeletonThemeProvider } from './components/Skeletons'
|
||||
import { DebugBus } from './utils/debugBus'
|
||||
import { Bookmark } from './types/bookmarks'
|
||||
import { fetchBookmarks } from './services/bookmarkService'
|
||||
import { bookmarkController } from './services/bookmarkController'
|
||||
|
||||
const DEFAULT_ARTICLE = import.meta.env.VITE_DEFAULT_ARTICLE_NADDR ||
|
||||
'naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqqxnzd3cxqmrzv3exgmr2wfesgsmew'
|
||||
@@ -36,60 +36,35 @@ function AppRoutes({
|
||||
const accountManager = Hooks.useAccountManager()
|
||||
const activeAccount = Hooks.useActiveAccount()
|
||||
|
||||
// Centralized bookmark state
|
||||
// Centralized bookmark state (fed by controller)
|
||||
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
|
||||
const [bookmarksLoading, setBookmarksLoading] = useState(false)
|
||||
const isLoadingRef = useRef(false)
|
||||
|
||||
// Load bookmarks function
|
||||
const loadBookmarks = useCallback(async () => {
|
||||
if (!relayPool || !activeAccount || isLoadingRef.current) return
|
||||
|
||||
try {
|
||||
isLoadingRef.current = true
|
||||
setBookmarksLoading(true)
|
||||
console.log('[app] 🔍 Loading bookmarks for', activeAccount.pubkey.slice(0, 8))
|
||||
|
||||
const fullAccount = accountManager.getActive()
|
||||
// Progressive updates via onProgressUpdate callback
|
||||
await fetchBookmarks(
|
||||
relayPool,
|
||||
fullAccount || activeAccount,
|
||||
accountManager,
|
||||
setBookmarks,
|
||||
undefined, // settings
|
||||
() => {
|
||||
// Trigger re-render on each event/decrypt (progressive loading)
|
||||
setBookmarksLoading(true)
|
||||
}
|
||||
)
|
||||
|
||||
console.log('[app] ✅ Bookmarks loaded')
|
||||
} catch (error) {
|
||||
console.error('[app] ❌ Failed to load bookmarks:', error)
|
||||
} finally {
|
||||
setBookmarksLoading(false)
|
||||
isLoadingRef.current = false
|
||||
}
|
||||
}, [relayPool, activeAccount, accountManager])
|
||||
|
||||
// Refresh bookmarks (for manual refresh button)
|
||||
const handleRefreshBookmarks = useCallback(async () => {
|
||||
console.log('[app] 🔄 Manual refresh triggered')
|
||||
await loadBookmarks()
|
||||
}, [loadBookmarks])
|
||||
|
||||
// Load bookmarks when account changes (includes initial mount)
|
||||
// Subscribe to bookmark controller
|
||||
useEffect(() => {
|
||||
if (activeAccount && relayPool) {
|
||||
console.log('[app] 👤 Loading bookmarks (account + relayPool ready)')
|
||||
loadBookmarks()
|
||||
const unsubBookmarks = bookmarkController.onBookmarks(setBookmarks)
|
||||
const unsubLoading = bookmarkController.onLoading(setBookmarksLoading)
|
||||
|
||||
return () => {
|
||||
unsubBookmarks()
|
||||
unsubLoading()
|
||||
}
|
||||
}, [activeAccount, relayPool, loadBookmarks])
|
||||
}, [])
|
||||
|
||||
// Manual refresh (for sidebar button)
|
||||
const handleRefreshBookmarks = useCallback(async () => {
|
||||
if (!relayPool || !activeAccount) {
|
||||
console.warn('[app] Cannot refresh: missing relayPool or activeAccount')
|
||||
return
|
||||
}
|
||||
console.log('[app] 🔄 Manual refresh triggered')
|
||||
bookmarkController.reset()
|
||||
await bookmarkController.start({ relayPool, activeAccount, accountManager })
|
||||
}, [relayPool, activeAccount, accountManager])
|
||||
|
||||
const handleLogout = () => {
|
||||
accountManager.clearActive()
|
||||
setBookmarks([]) // Clear bookmarks on logout
|
||||
bookmarkController.reset() // Clear bookmarks via controller
|
||||
showToast('Logged out successfully')
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ import { Helpers } from 'applesauce-core'
|
||||
import { getDefaultBunkerPermissions } from '../services/nostrConnect'
|
||||
import { DebugBus, type DebugLogEntry } from '../utils/debugBus'
|
||||
import ThreePaneLayout from './ThreePaneLayout'
|
||||
import { queryEvents } from '../services/dataFetch'
|
||||
import { KINDS } from '../config/kinds'
|
||||
import { collectBookmarksFromEvents } from '../services/bookmarkProcessing'
|
||||
import type { NostrEvent } from '../services/bookmarkHelpers'
|
||||
import { Bookmark } from '../types/bookmarks'
|
||||
import { useBookmarksUI } from '../hooks/useBookmarksUI'
|
||||
@@ -244,79 +242,49 @@ const Debug: React.FC<DebugProps> = ({
|
||||
setIsLoadingBookmarks(true)
|
||||
setBookmarkStats(null)
|
||||
setBookmarkEvents([]) // Clear existing events
|
||||
setDecryptedEvents(new Map())
|
||||
DebugBus.info('debug', 'Loading bookmark events...')
|
||||
|
||||
// Start timing
|
||||
const start = performance.now()
|
||||
setLiveTiming(prev => ({ ...prev, loadBookmarks: { startTime: start } }))
|
||||
|
||||
// Get signer for auto-decryption
|
||||
const fullAccount = accountManager.getActive()
|
||||
const signerCandidate = fullAccount || activeAccount
|
||||
|
||||
// Use onEvent callback to stream events as they arrive
|
||||
// Trust EOSE - completes when relays finish, no artificial timeouts
|
||||
const rawEvents = await queryEvents(
|
||||
relayPool,
|
||||
{ kinds: [KINDS.ListSimple, KINDS.ListReplaceable, KINDS.List, KINDS.WebBookmark], authors: [activeAccount.pubkey] },
|
||||
{
|
||||
onEvent: async (evt) => {
|
||||
// Add event immediately with live deduplication
|
||||
setBookmarkEvents(prev => {
|
||||
// Create unique key for deduplication
|
||||
const key = getEventKey(evt)
|
||||
|
||||
// Find existing event with same key
|
||||
const existingIdx = prev.findIndex(e => getEventKey(e) === key)
|
||||
|
||||
if (existingIdx >= 0) {
|
||||
// Replace if newer
|
||||
const existing = prev[existingIdx]
|
||||
if ((evt.created_at || 0) > (existing.created_at || 0)) {
|
||||
const newEvents = [...prev]
|
||||
newEvents[existingIdx] = evt
|
||||
return newEvents
|
||||
}
|
||||
return prev // Keep existing (it's newer)
|
||||
}
|
||||
|
||||
// Add new event
|
||||
return [...prev, evt]
|
||||
})
|
||||
|
||||
// Auto-decrypt if event has encrypted content
|
||||
if (hasEncryptedContent(evt)) {
|
||||
console.log('[bunker] 🔓 Auto-decrypting event', evt.id.slice(0, 8))
|
||||
try {
|
||||
const { publicItemsAll, privateItemsAll } = await collectBookmarksFromEvents(
|
||||
[evt],
|
||||
activeAccount,
|
||||
signerCandidate
|
||||
)
|
||||
setDecryptedEvents(prev => new Map(prev).set(evt.id, {
|
||||
public: publicItemsAll.length,
|
||||
private: privateItemsAll.length
|
||||
}))
|
||||
console.log('[bunker] ✅ Auto-decrypted:', evt.id.slice(0, 8), {
|
||||
public: publicItemsAll.length,
|
||||
private: privateItemsAll.length
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[bunker] ❌ Auto-decrypt failed:', evt.id.slice(0, 8), error)
|
||||
}
|
||||
// Import controller at runtime to avoid circular dependencies
|
||||
const { bookmarkController } = await import('../services/bookmarkController')
|
||||
|
||||
// Subscribe to raw events for Debug UI display
|
||||
const unsubscribe = bookmarkController.onRawEvent((evt) => {
|
||||
// Add event immediately with live deduplication
|
||||
setBookmarkEvents(prev => {
|
||||
const key = getEventKey(evt)
|
||||
const existingIdx = prev.findIndex(e => getEventKey(e) === key)
|
||||
|
||||
if (existingIdx >= 0) {
|
||||
const existing = prev[existingIdx]
|
||||
if ((evt.created_at || 0) > (existing.created_at || 0)) {
|
||||
const newEvents = [...prev]
|
||||
newEvents[existingIdx] = evt
|
||||
return newEvents
|
||||
}
|
||||
return prev
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return [...prev, evt]
|
||||
})
|
||||
})
|
||||
|
||||
// Start the controller (triggers app bookmark population too)
|
||||
bookmarkController.reset()
|
||||
await bookmarkController.start({ relayPool, activeAccount, accountManager })
|
||||
|
||||
// Clean up subscription
|
||||
unsubscribe()
|
||||
|
||||
const ms = Math.round(performance.now() - start)
|
||||
setLiveTiming(prev => ({ ...prev, loadBookmarks: undefined }))
|
||||
setTLoadBookmarks(ms)
|
||||
|
||||
DebugBus.info('debug', `Loaded ${rawEvents.length} bookmark events`, {
|
||||
kinds: rawEvents.map(e => e.kind).join(', '),
|
||||
ms
|
||||
})
|
||||
DebugBus.info('debug', `Loaded bookmark events`, { ms })
|
||||
} catch (error) {
|
||||
setLiveTiming(prev => ({ ...prev, loadBookmarks: undefined }))
|
||||
DebugBus.error('debug', 'Failed to load bookmarks', error instanceof Error ? error.message : String(error))
|
||||
|
||||
331
src/services/bookmarkController.ts
Normal file
331
src/services/bookmarkController.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
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'
|
||||
import { Bookmark } from '../types/bookmarks'
|
||||
import {
|
||||
AccountWithExtension,
|
||||
hydrateItems,
|
||||
dedupeBookmarksById,
|
||||
extractUrlsFromContent
|
||||
} from './bookmarkHelpers'
|
||||
|
||||
/**
|
||||
* Get unique key for event deduplication (from Debug)
|
||||
*/
|
||||
function getEventKey(evt: NostrEvent): string {
|
||||
if (evt.kind === 30003 || evt.kind === 30001) {
|
||||
const dTag = evt.tags?.find((t: string[]) => t[0] === 'd')?.[1] || ''
|
||||
return `${evt.kind}:${evt.pubkey}:${dTag}`
|
||||
} else if (evt.kind === 10003) {
|
||||
return `${evt.kind}:${evt.pubkey}`
|
||||
}
|
||||
return evt.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event has encrypted content (from Debug)
|
||||
*/
|
||||
function hasEncryptedContent(evt: NostrEvent): boolean {
|
||||
if (Helpers.hasHiddenContent(evt)) return true
|
||||
if (evt.content && evt.content.includes('?iv=')) return true
|
||||
if (Helpers.hasHiddenTags(evt) && !Helpers.isHiddenTagsUnlocked(evt)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
type RawEventCallback = (event: NostrEvent) => void
|
||||
type BookmarksCallback = (bookmarks: Bookmark[]) => void
|
||||
type LoadingCallback = (loading: boolean) => void
|
||||
|
||||
/**
|
||||
* Shared bookmark streaming controller
|
||||
* Encapsulates the Debug flow: stream events, dedupe, decrypt, build bookmarks
|
||||
*/
|
||||
class BookmarkController {
|
||||
private rawEventListeners: RawEventCallback[] = []
|
||||
private bookmarksListeners: BookmarksCallback[] = []
|
||||
private loadingListeners: LoadingCallback[] = []
|
||||
|
||||
private currentEvents: Map<string, NostrEvent> = new Map()
|
||||
private decryptedEvents: Map<string, { public: number; private: number }> = new Map()
|
||||
private isLoading = false
|
||||
private updateScheduled = false
|
||||
|
||||
onRawEvent(cb: RawEventCallback): () => void {
|
||||
this.rawEventListeners.push(cb)
|
||||
return () => {
|
||||
this.rawEventListeners = this.rawEventListeners.filter(l => l !== cb)
|
||||
}
|
||||
}
|
||||
|
||||
onBookmarks(cb: BookmarksCallback): () => void {
|
||||
this.bookmarksListeners.push(cb)
|
||||
return () => {
|
||||
this.bookmarksListeners = this.bookmarksListeners.filter(l => l !== cb)
|
||||
}
|
||||
}
|
||||
|
||||
onLoading(cb: LoadingCallback): () => void {
|
||||
this.loadingListeners.push(cb)
|
||||
return () => {
|
||||
this.loadingListeners = this.loadingListeners.filter(l => l !== cb)
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.currentEvents.clear()
|
||||
this.decryptedEvents.clear()
|
||||
this.setLoading(false)
|
||||
this.updateScheduled = false
|
||||
}
|
||||
|
||||
private setLoading(loading: boolean): void {
|
||||
if (this.isLoading !== loading) {
|
||||
this.isLoading = loading
|
||||
this.loadingListeners.forEach(cb => cb(loading))
|
||||
}
|
||||
}
|
||||
|
||||
private emitRawEvent(evt: NostrEvent): void {
|
||||
this.rawEventListeners.forEach(cb => cb(evt))
|
||||
}
|
||||
|
||||
private scheduleBookmarkUpdate(
|
||||
relayPool: RelayPool,
|
||||
activeAccount: AccountWithExtension,
|
||||
signerCandidate: unknown
|
||||
): void {
|
||||
if (this.updateScheduled) return
|
||||
|
||||
this.updateScheduled = true
|
||||
setTimeout(async () => {
|
||||
this.updateScheduled = false
|
||||
await this.buildAndEmitBookmarks(relayPool, activeAccount, signerCandidate)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
private async buildAndEmitBookmarks(
|
||||
relayPool: RelayPool,
|
||||
activeAccount: AccountWithExtension,
|
||||
signerCandidate: unknown
|
||||
): Promise<void> {
|
||||
const events = Array.from(this.currentEvents.values())
|
||||
if (events.length === 0) {
|
||||
this.bookmarksListeners.forEach(cb => cb([]))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 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('[controller] 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('[controller] 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
|
||||
}
|
||||
|
||||
console.log('[controller] 📋 Built bookmark with', sortedBookmarks.length, 'items')
|
||||
this.bookmarksListeners.forEach(cb => cb([bookmark]))
|
||||
} catch (error) {
|
||||
console.error('[controller] ❌ Failed to build bookmarks:', error)
|
||||
this.bookmarksListeners.forEach(cb => cb([]))
|
||||
}
|
||||
}
|
||||
|
||||
async start(options: {
|
||||
relayPool: RelayPool
|
||||
activeAccount: unknown
|
||||
accountManager: { getActive: () => unknown }
|
||||
}): Promise<void> {
|
||||
const { relayPool, activeAccount, accountManager } = options
|
||||
|
||||
if (!activeAccount || typeof (activeAccount as { pubkey?: string }).pubkey !== 'string') {
|
||||
console.error('[controller] Invalid activeAccount')
|
||||
return
|
||||
}
|
||||
|
||||
const account = activeAccount as { pubkey: string; [key: string]: unknown }
|
||||
|
||||
this.setLoading(true)
|
||||
console.log('[controller] 🔍 Starting bookmark load for', account.pubkey.slice(0, 8))
|
||||
|
||||
try {
|
||||
// Get signer for auto-decryption
|
||||
const fullAccount = accountManager.getActive() as AccountWithExtension | null
|
||||
const maybeAccount = (fullAccount || account) 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
|
||||
}
|
||||
|
||||
// Stream events with live deduplication (same as Debug)
|
||||
await queryEvents(
|
||||
relayPool,
|
||||
{ kinds: [KINDS.ListSimple, KINDS.ListReplaceable, KINDS.List, KINDS.WebBookmark], authors: [account.pubkey] },
|
||||
{
|
||||
onEvent: async (evt) => {
|
||||
const key = getEventKey(evt)
|
||||
const existing = this.currentEvents.get(key)
|
||||
|
||||
if (existing && (existing.created_at || 0) >= (evt.created_at || 0)) {
|
||||
return // Keep existing (it's newer)
|
||||
}
|
||||
|
||||
// Add/update event
|
||||
this.currentEvents.set(key, evt)
|
||||
console.log('[controller] 📨 Event:', evt.kind, evt.id.slice(0, 8), 'encrypted:', hasEncryptedContent(evt))
|
||||
|
||||
// Emit raw event for Debug UI
|
||||
this.emitRawEvent(evt)
|
||||
|
||||
// Schedule bookmark update (non-blocking, coalesced)
|
||||
this.scheduleBookmarkUpdate(relayPool, maybeAccount, signerCandidate)
|
||||
|
||||
// Auto-decrypt if event has encrypted content
|
||||
if (hasEncryptedContent(evt)) {
|
||||
console.log('[controller] 🔓 Auto-decrypting event', evt.id.slice(0, 8))
|
||||
try {
|
||||
const { publicItemsAll, privateItemsAll } = await collectBookmarksFromEvents(
|
||||
[evt],
|
||||
account,
|
||||
signerCandidate
|
||||
)
|
||||
this.decryptedEvents.set(evt.id, {
|
||||
public: publicItemsAll.length,
|
||||
private: privateItemsAll.length
|
||||
})
|
||||
console.log('[controller] ✅ Auto-decrypted:', evt.id.slice(0, 8), {
|
||||
public: publicItemsAll.length,
|
||||
private: privateItemsAll.length
|
||||
})
|
||||
|
||||
// Schedule another update after decrypt
|
||||
this.scheduleBookmarkUpdate(relayPool, maybeAccount, signerCandidate)
|
||||
} catch (error) {
|
||||
console.error('[controller] ❌ Auto-decrypt failed:', evt.id.slice(0, 8), error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Final update after EOSE
|
||||
await this.buildAndEmitBookmarks(relayPool, maybeAccount, signerCandidate)
|
||||
console.log('[controller] ✅ Bookmark load complete')
|
||||
} catch (error) {
|
||||
console.error('[controller] ❌ Failed to load bookmarks:', error)
|
||||
this.bookmarksListeners.forEach(cb => cb([]))
|
||||
} finally {
|
||||
this.setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const bookmarkController = new BookmarkController()
|
||||
|
||||
Reference in New Issue
Block a user