import React, { useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faClock, faSpinner } from '@fortawesome/free-solid-svg-icons' import { Hooks } from 'applesauce-react' import { Accounts } from 'applesauce-accounts' import { NostrConnectSigner } from 'applesauce-signers' import { RelayPool } from 'applesauce-relay' import { Helpers, IEventStore } from 'applesauce-core' import { nip19 } from 'nostr-tools' import { getDefaultBunkerPermissions } from '../services/nostrConnect' import { DebugBus, type DebugLogEntry } from '../utils/debugBus' import ThreePaneLayout from './ThreePaneLayout' import { KINDS } from '../config/kinds' import type { NostrEvent } from '../services/bookmarkHelpers' import { Bookmark } from '../types/bookmarks' import { useBookmarksUI } from '../hooks/useBookmarksUI' import { useSettings } from '../hooks/useSettings' import { fetchHighlights, fetchHighlightsFromAuthors } from '../services/highlightService' import { contactsController } from '../services/contactsController' import { writingsController } from '../services/writingsController' import { fetchBlogPostsFromAuthors, BlogPostPreview } from '../services/exploreService' const defaultPayload = 'The quick brown fox jumps over the lazy dog.' interface DebugProps { relayPool: RelayPool | null eventStore: IEventStore | null bookmarks: Bookmark[] bookmarksLoading: boolean onRefreshBookmarks: () => Promise onLogout: () => void } const Debug: React.FC = ({ relayPool, eventStore, bookmarks, bookmarksLoading, onRefreshBookmarks, onLogout }) => { const navigate = useNavigate() const activeAccount = Hooks.useActiveAccount() const accountManager = Hooks.useAccountManager() const { settings, saveSettings } = useSettings({ relayPool, eventStore: eventStore!, pubkey: activeAccount?.pubkey, accountManager }) const { isMobile, isCollapsed, setIsCollapsed, viewMode, setViewMode } = useBookmarksUI({ settings }) const [payload, setPayload] = useState(defaultPayload) const [cipher44, setCipher44] = useState('') const [cipher04, setCipher04] = useState('') const [plain44, setPlain44] = useState('') const [plain04, setPlain04] = useState('') const [tEncrypt44, setTEncrypt44] = useState(null) const [tEncrypt04, setTEncrypt04] = useState(null) const [tDecrypt44, setTDecrypt44] = useState(null) const [tDecrypt04, setTDecrypt04] = useState(null) const [logs, setLogs] = useState(DebugBus.snapshot()) const [debugEnabled, setDebugEnabled] = useState(() => localStorage.getItem('debug') === '*') // Bunker login state const [bunkerUri, setBunkerUri] = useState('') const [isBunkerLoading, setIsBunkerLoading] = useState(false) const [bunkerError, setBunkerError] = useState(null) // Bookmark loading state const [bookmarkEvents, setBookmarkEvents] = useState([]) const [isLoadingBookmarks, setIsLoadingBookmarks] = useState(false) const [bookmarkStats, setBookmarkStats] = useState<{ public: number; private: number } | null>(null) const [tLoadBookmarks, setTLoadBookmarks] = useState(null) const [tDecryptBookmarks, setTDecryptBookmarks] = useState(null) const [tFirstBookmark, setTFirstBookmark] = useState(null) // Individual event decryption results const [decryptedEvents, setDecryptedEvents] = useState>(new Map()) // Highlight loading state const [highlightMode, setHighlightMode] = useState<'article' | 'url' | 'author'>('author') const [highlightArticleCoord, setHighlightArticleCoord] = useState('') const [highlightUrl, setHighlightUrl] = useState('') const [highlightAuthor, setHighlightAuthor] = useState('') const [isLoadingHighlights, setIsLoadingHighlights] = useState(false) const [highlightEvents, setHighlightEvents] = useState([]) const [tLoadHighlights, setTLoadHighlights] = useState(null) const [tFirstHighlight, setTFirstHighlight] = useState(null) // Writings loading state const [isLoadingWritings, setIsLoadingWritings] = useState(false) const [writingPosts, setWritingPosts] = useState([]) const [tLoadWritings, setTLoadWritings] = useState(null) const [tFirstWriting, setTFirstWriting] = useState(null) // Live timing state const [liveTiming, setLiveTiming] = useState<{ nip44?: { type: 'encrypt' | 'decrypt'; startTime: number } nip04?: { type: 'encrypt' | 'decrypt'; startTime: number } loadBookmarks?: { startTime: number } decryptBookmarks?: { startTime: number } loadHighlights?: { startTime: number } }>({}) // Web of Trust state const [friendsPubkeys, setFriendsPubkeys] = useState>(new Set()) const [friendsButtonLoading, setFriendsButtonLoading] = useState(false) useEffect(() => { return DebugBus.subscribe((e) => setLogs(prev => [...prev, e].slice(-300))) }, []) // Live timer effect - triggers re-renders for live timing updates useEffect(() => { const interval = setInterval(() => { // Force re-render to update live timing display setLiveTiming(prev => prev) }, 16) // ~60fps for smooth updates return () => clearInterval(interval) }, []) const signer = useMemo(() => (activeAccount as unknown as { signer?: unknown })?.signer, [activeAccount]) const pubkey = (activeAccount as unknown as { pubkey?: string })?.pubkey const hasNip04 = typeof (signer as { nip04?: { encrypt?: unknown; decrypt?: unknown } } | undefined)?.nip04?.encrypt === 'function' const hasNip44 = typeof (signer as { nip44?: { encrypt?: unknown; decrypt?: unknown } } | undefined)?.nip44?.encrypt === 'function' const getKindName = (kind: number): string => { switch (kind) { case KINDS.ListSimple: return 'Simple List (10003)' case KINDS.ListReplaceable: return 'Replaceable List (30003)' case KINDS.List: return 'List (30001)' case KINDS.WebBookmark: return 'Web Bookmark (39701)' default: return `Kind ${kind}` } } const getEventSize = (evt: NostrEvent): number => { const content = evt.content || '' const tags = JSON.stringify(evt.tags || []) return content.length + tags.length } const 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 } const getBookmarkCount = (evt: NostrEvent): { public: number; private: number } => { const publicTags = (evt.tags || []).filter((t: string[]) => t[0] === 'e' || t[0] === 'a') const hasEncrypted = hasEncryptedContent(evt) return { public: publicTags.length, private: hasEncrypted ? 1 : 0 // Can't know exact count until decrypted } } const formatBytes = (bytes: number): string => { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / (1024 * 1024)).toFixed(2)} MB` } const 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 } const doEncrypt = async (mode: 'nip44' | 'nip04') => { if (!signer || !pubkey) return try { const api = (signer as { [key: string]: { encrypt: (pubkey: string, message: string) => Promise } })[mode] DebugBus.info('debug', `encrypt start ${mode}`, { pubkey, len: payload.length }) // Start live timing const start = performance.now() setLiveTiming(prev => ({ ...prev, [mode]: { type: 'encrypt', startTime: start } })) const cipher = await api.encrypt(pubkey, payload) const ms = Math.round(performance.now() - start) // Stop live timing setLiveTiming(prev => ({ ...prev, [mode]: undefined })) DebugBus.info('debug', `encrypt done ${mode}`, { len: typeof cipher === 'string' ? cipher.length : -1, ms }) if (mode === 'nip44') setCipher44(cipher) else setCipher04(cipher) if (mode === 'nip44') setTEncrypt44(ms) else setTEncrypt04(ms) } catch (e) { // Stop live timing on error setLiveTiming(prev => ({ ...prev, [mode]: undefined })) DebugBus.error('debug', `encrypt error ${mode}`, e instanceof Error ? e.message : String(e)) } } const doDecrypt = async (mode: 'nip44' | 'nip04') => { if (!signer || !pubkey) return try { const api = (signer as { [key: string]: { decrypt: (pubkey: string, ciphertext: string) => Promise } })[mode] const cipher = mode === 'nip44' ? cipher44 : cipher04 if (!cipher) { DebugBus.warn('debug', `no cipher to decrypt for ${mode}`) return } DebugBus.info('debug', `decrypt start ${mode}`, { len: cipher.length }) // Start live timing const start = performance.now() setLiveTiming(prev => ({ ...prev, [mode]: { type: 'decrypt', startTime: start } })) const plain = await api.decrypt(pubkey, cipher) const ms = Math.round(performance.now() - start) // Stop live timing setLiveTiming(prev => ({ ...prev, [mode]: undefined })) DebugBus.info('debug', `decrypt done ${mode}`, { len: typeof plain === 'string' ? plain.length : -1, ms }) if (mode === 'nip44') setPlain44(String(plain)) else setPlain04(String(plain)) if (mode === 'nip44') setTDecrypt44(ms) else setTDecrypt04(ms) } catch (e) { // Stop live timing on error setLiveTiming(prev => ({ ...prev, [mode]: undefined })) DebugBus.error('debug', `decrypt error ${mode}`, e instanceof Error ? e.message : String(e)) } } const toggleDebug = () => { const next = !debugEnabled setDebugEnabled(next) if (next) localStorage.setItem('debug', '*') else localStorage.removeItem('debug') } const handleLoadBookmarks = async () => { if (!relayPool || !activeAccount) { DebugBus.warn('debug', 'Cannot load bookmarks: missing relayPool or activeAccount') return } try { setIsLoadingBookmarks(true) setBookmarkStats(null) setBookmarkEvents([]) // Clear existing events setDecryptedEvents(new Map()) setTFirstBookmark(null) DebugBus.info('debug', 'Loading bookmark events...') // Start timing const start = performance.now() let firstEventTime: number | null = null setLiveTiming(prev => ({ ...prev, loadBookmarks: { startTime: start } })) // Import controller at runtime to avoid circular dependencies const { bookmarkController } = await import('../services/bookmarkController') // Subscribe to raw events for Debug UI display const unsubscribeRaw = bookmarkController.onRawEvent((evt) => { // Track time to first event if (firstEventTime === null) { firstEventTime = performance.now() - start setTFirstBookmark(Math.round(firstEventTime)) } // 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] }) }) // Subscribe to decrypt complete events for Debug UI display const unsubscribeDecrypt = bookmarkController.onDecryptComplete((eventId, publicCount, privateCount) => { console.log('[bunker] ✅ Auto-decrypted:', eventId.slice(0, 8), { public: publicCount, private: privateCount }) setDecryptedEvents(prev => new Map(prev).set(eventId, { public: publicCount, private: privateCount })) }) // Start the controller (triggers app bookmark population too) bookmarkController.reset() await bookmarkController.start({ relayPool, activeAccount, accountManager }) // Clean up subscriptions unsubscribeRaw() unsubscribeDecrypt() const ms = Math.round(performance.now() - start) setLiveTiming(prev => ({ ...prev, loadBookmarks: undefined })) setTLoadBookmarks(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)) } finally { setIsLoadingBookmarks(false) } } const handleClearBookmarks = () => { setBookmarkEvents([]) setBookmarkStats(null) setTLoadBookmarks(null) setTDecryptBookmarks(null) setTFirstBookmark(null) setDecryptedEvents(new Map()) DebugBus.info('debug', 'Cleared bookmark data') } const handleLoadHighlights = async () => { if (!relayPool) { DebugBus.warn('debug', 'Cannot load highlights: missing relayPool') return } // Default to logged-in user's highlights if no specific query provided const getValue = () => { if (highlightMode === 'article') return highlightArticleCoord.trim() if (highlightMode === 'url') return highlightUrl.trim() const authorValue = highlightAuthor.trim() return authorValue || pubkey || '' } const value = getValue() if (!value) { DebugBus.warn('debug', 'Please provide a value to query or log in') return } try { setIsLoadingHighlights(true) setHighlightEvents([]) setTFirstHighlight(null) DebugBus.info('debug', `Loading highlights (${highlightMode}: ${value})...`) const start = performance.now() setLiveTiming(prev => ({ ...prev, loadHighlights: { startTime: start } })) let firstEventTime: number | null = null const seenIds = new Set() // Import highlight services const { queryEvents } = await import('../services/dataFetch') const { KINDS } = await import('../config/kinds') // Build filter based on mode let filter: { kinds: number[]; '#a'?: string[]; '#r'?: string[]; authors?: string[] } if (highlightMode === 'article') { filter = { kinds: [KINDS.Highlights], '#a': [value] } } else if (highlightMode === 'url') { filter = { kinds: [KINDS.Highlights], '#r': [value] } } else { filter = { kinds: [KINDS.Highlights], authors: [value] } } const events = await queryEvents(relayPool, filter, { onEvent: (evt) => { if (seenIds.has(evt.id)) return seenIds.add(evt.id) if (firstEventTime === null) { firstEventTime = performance.now() - start setTFirstHighlight(Math.round(firstEventTime)) } setHighlightEvents(prev => [...prev, evt]) } }) const elapsed = Math.round(performance.now() - start) setTLoadHighlights(elapsed) setLiveTiming(prev => { // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars const { loadHighlights, ...rest } = prev return rest }) DebugBus.info('debug', `Loaded ${events.length} highlight events in ${elapsed}ms`) } catch (err) { console.error('Failed to load highlights:', err) DebugBus.error('debug', `Failed to load highlights: ${err instanceof Error ? err.message : String(err)}`) } finally { setIsLoadingHighlights(false) } } const handleClearHighlights = () => { setHighlightEvents([]) setTLoadHighlights(null) setTFirstHighlight(null) DebugBus.info('debug', 'Cleared highlight data') } const handleLoadMyHighlights = async () => { if (!relayPool || !activeAccount?.pubkey) { DebugBus.warn('debug', 'Please log in to load your highlights') return } const start = performance.now() setHighlightEvents([]) setIsLoadingHighlights(true) setTLoadHighlights(null) setTFirstHighlight(null) DebugBus.info('debug', 'Loading my highlights...') try { let firstEventTime: number | null = null await fetchHighlights(relayPool, activeAccount.pubkey, (h) => { if (firstEventTime === null) { firstEventTime = performance.now() - start setTFirstHighlight(Math.round(firstEventTime)) } setHighlightEvents(prev => { if (prev.some(x => x.id === h.id)) return prev const next = [...prev, { ...h, pubkey: h.pubkey, created_at: h.created_at, id: h.id, kind: 9802, tags: [], content: h.content, sig: '' } as NostrEvent] return next.sort((a, b) => b.created_at - a.created_at) }) }, settings, false, eventStore || undefined) } finally { setIsLoadingHighlights(false) const elapsed = Math.round(performance.now() - start) setTLoadHighlights(elapsed) DebugBus.info('debug', `Loaded my highlights in ${elapsed}ms`) } } const handleLoadFriendsHighlights = async () => { if (!relayPool || !activeAccount?.pubkey) { DebugBus.warn('debug', 'Please log in to load friends highlights') return } // Get contacts from centralized controller (should already be loaded by App.tsx) const contacts = contactsController.getContacts() if (contacts.size === 0) { DebugBus.warn('debug', 'No friends found. Make sure you have contacts loaded.') return } const start = performance.now() setHighlightEvents([]) setIsLoadingHighlights(true) setTLoadHighlights(null) setTFirstHighlight(null) DebugBus.info('debug', `Loading highlights from ${contacts.size} friends (using cached contacts)...`) let firstEventTime: number | null = null try { await fetchHighlightsFromAuthors(relayPool, Array.from(contacts), (h) => { if (firstEventTime === null) { firstEventTime = performance.now() - start setTFirstHighlight(Math.round(firstEventTime)) } setHighlightEvents(prev => { if (prev.some(x => x.id === h.id)) return prev const next = [...prev, { ...h, pubkey: h.pubkey, created_at: h.created_at, id: h.id, kind: 9802, tags: [], content: h.content, sig: '' } as NostrEvent] return next.sort((a, b) => b.created_at - a.created_at) }) }, eventStore || undefined) } finally { setIsLoadingHighlights(false) const elapsed = Math.round(performance.now() - start) setTLoadHighlights(elapsed) DebugBus.info('debug', `Loaded friends highlights in ${elapsed}ms`) } } const handleLoadNostrverseHighlights = async () => { if (!relayPool) { DebugBus.warn('debug', 'Relay pool not available') return } const start = performance.now() setHighlightEvents([]) setIsLoadingHighlights(true) setTLoadHighlights(null) setTFirstHighlight(null) DebugBus.info('debug', 'Loading nostrverse highlights (kind:9802)...') try { let firstEventTime: number | null = null const seenIds = new Set() const { queryEvents } = await import('../services/dataFetch') const events = await queryEvents(relayPool, { kinds: [9802], limit: 500 }, { onEvent: (evt) => { if (seenIds.has(evt.id)) return seenIds.add(evt.id) if (firstEventTime === null) { firstEventTime = performance.now() - start setTFirstHighlight(Math.round(firstEventTime)) } setHighlightEvents(prev => [...prev, evt]) } }) DebugBus.info('debug', `Loaded ${events.length} nostrverse highlights`) } finally { setIsLoadingHighlights(false) const elapsed = Math.round(performance.now() - start) setTLoadHighlights(elapsed) DebugBus.info('debug', `Loaded nostrverse highlights in ${elapsed}ms`) } } const handleLoadMyWritings = async () => { if (!relayPool || !activeAccount?.pubkey || !eventStore) { DebugBus.warn('debug', 'Please log in to load your writings') return } const start = performance.now() setWritingPosts([]) setIsLoadingWritings(true) setTLoadWritings(null) setTFirstWriting(null) DebugBus.info('debug', 'Loading my writings via writingsController...') try { let firstEventTime: number | null = null const unsub = writingsController.onWritings((posts) => { if (firstEventTime === null && posts.length > 0) { firstEventTime = performance.now() - start setTFirstWriting(Math.round(firstEventTime)) } setWritingPosts(posts) }) await writingsController.start({ relayPool, eventStore, pubkey: activeAccount.pubkey, force: true }) unsub() const currentWritings = writingsController.getWritings() setWritingPosts(currentWritings) DebugBus.info('debug', `Loaded ${currentWritings.length} writings via controller`) } finally { setIsLoadingWritings(false) const elapsed = Math.round(performance.now() - start) setTLoadWritings(elapsed) DebugBus.info('debug', `Loaded my writings in ${elapsed}ms`) } } const handleLoadFriendsWritings = async () => { if (!relayPool || !activeAccount?.pubkey) { DebugBus.warn('debug', 'Please log in to load friends writings') return } const start = performance.now() setWritingPosts([]) setIsLoadingWritings(true) setTLoadWritings(null) setTFirstWriting(null) DebugBus.info('debug', 'Loading friends writings...') try { // Get contacts first await contactsController.start({ relayPool, pubkey: activeAccount.pubkey }) const friends = contactsController.getContacts() const friendsArray = Array.from(friends) DebugBus.info('debug', `Found ${friendsArray.length} friends`) if (friendsArray.length === 0) { DebugBus.warn('debug', 'No friends found to load writings from') return } let firstEventTime: number | null = null const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url) const posts = await fetchBlogPostsFromAuthors( relayPool, friendsArray, relayUrls, (post) => { if (firstEventTime === null) { firstEventTime = performance.now() - start setTFirstWriting(Math.round(firstEventTime)) } setWritingPosts(prev => { const dTag = post.event.tags.find(t => t[0] === 'd')?.[1] || '' const key = `${post.author}:${dTag}` const exists = prev.find(p => { const pDTag = p.event.tags.find(t => t[0] === 'd')?.[1] || '' return `${p.author}:${pDTag}` === key }) if (exists) return prev return [...prev, post].sort((a, b) => { const timeA = a.published || a.event.created_at const timeB = b.published || b.event.created_at return timeB - timeA }) }) } ) setWritingPosts(posts) DebugBus.info('debug', `Loaded ${posts.length} friend writings`) } finally { setIsLoadingWritings(false) const elapsed = Math.round(performance.now() - start) setTLoadWritings(elapsed) DebugBus.info('debug', `Loaded friend writings in ${elapsed}ms`) } } const handleLoadNostrverseWritings = async () => { if (!relayPool) { DebugBus.warn('debug', 'Relay pool not available') return } const start = performance.now() setWritingPosts([]) setIsLoadingWritings(true) setTLoadWritings(null) setTFirstWriting(null) DebugBus.info('debug', 'Loading nostrverse writings (kind:30023)...') try { let firstEventTime: number | null = null const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url) const { queryEvents } = await import('../services/dataFetch') const { Helpers } = await import('applesauce-core') const { getArticleTitle, getArticleSummary, getArticleImage, getArticlePublished } = Helpers const uniqueEvents = new Map() await queryEvents(relayPool, { kinds: [30023], limit: 50 }, { relayUrls, onEvent: (evt) => { const dTag = evt.tags.find(t => t[0] === 'd')?.[1] || '' const key = `${evt.pubkey}:${dTag}` const existing = uniqueEvents.get(key) if (!existing || evt.created_at > existing.created_at) { uniqueEvents.set(key, evt) if (firstEventTime === null) { firstEventTime = performance.now() - start setTFirstWriting(Math.round(firstEventTime)) } const posts = Array.from(uniqueEvents.values()).map(event => ({ event, title: getArticleTitle(event) || 'Untitled', summary: getArticleSummary(event), image: getArticleImage(event), published: getArticlePublished(event), author: event.pubkey } as BlogPostPreview)).sort((a, b) => { const timeA = a.published || a.event.created_at const timeB = b.published || b.event.created_at return timeB - timeA }) setWritingPosts(posts) } } }) const finalPosts = Array.from(uniqueEvents.values()).map(event => ({ event, title: getArticleTitle(event) || 'Untitled', summary: getArticleSummary(event), image: getArticleImage(event), published: getArticlePublished(event), author: event.pubkey } as BlogPostPreview)).sort((a, b) => { const timeA = a.published || a.event.created_at const timeB = b.published || b.event.created_at return timeB - timeA }) setWritingPosts(finalPosts) DebugBus.info('debug', `Loaded ${finalPosts.length} nostrverse writings`) } finally { setIsLoadingWritings(false) const elapsed = Math.round(performance.now() - start) setTLoadWritings(elapsed) DebugBus.info('debug', `Loaded nostrverse writings in ${elapsed}ms`) } } const handleClearWritings = () => { setWritingPosts([]) setTLoadWritings(null) setTFirstWriting(null) } const handleLoadFriendsList = async () => { if (!relayPool || !activeAccount?.pubkey) { DebugBus.warn('debug', 'Please log in to load friends list') return } setFriendsButtonLoading(true) DebugBus.info('debug', 'Loading friends list via controller...') // Clear current list setFriendsPubkeys(new Set()) // Subscribe to controller updates to see streaming const unsubscribe = contactsController.onContacts((contacts) => { console.log('[debug] Received contacts update:', contacts.size) setFriendsPubkeys(new Set(contacts)) }) try { // Force reload to see streaming behavior await contactsController.start({ relayPool, pubkey: activeAccount.pubkey, force: true }) const final = contactsController.getContacts() setFriendsPubkeys(new Set(final)) DebugBus.info('debug', `Loaded ${final.size} friends from controller`) } catch (err) { console.error('[debug] Failed to load friends:', err) DebugBus.error('debug', `Failed to load friends: ${err instanceof Error ? err.message : String(err)}`) } finally { unsubscribe() setFriendsButtonLoading(false) } } const friendsNpubs = useMemo(() => { return Array.from(friendsPubkeys).map(pk => nip19.npubEncode(pk)) }, [friendsPubkeys]) const handleBunkerLogin = async () => { if (!bunkerUri.trim()) { setBunkerError('Please enter a bunker URI') return } if (!bunkerUri.startsWith('bunker://')) { setBunkerError('Invalid bunker URI. Must start with bunker://') return } try { setIsBunkerLoading(true) setBunkerError(null) // Create signer from bunker URI with default permissions const permissions = getDefaultBunkerPermissions() const signer = await NostrConnectSigner.fromBunkerURI(bunkerUri, { permissions }) // Get pubkey from signer const pubkey = await signer.getPublicKey() // Create account from signer const account = new Accounts.NostrConnectAccount(pubkey, signer) // Add to account manager and set active accountManager.addAccount(account) accountManager.setActive(account) // Clear input on success setBunkerUri('') } catch (err) { console.error('[bunker] Login failed:', err) const errorMessage = err instanceof Error ? err.message : 'Failed to connect to bunker' // Check for permission-related errors if (errorMessage.toLowerCase().includes('permission') || errorMessage.toLowerCase().includes('unauthorized')) { setBunkerError('Your bunker connection is missing signing permissions. Reconnect and approve signing.') } else { setBunkerError(errorMessage) } } finally { setIsBunkerLoading(false) } } const CodeBox = ({ value }: { value: string }) => (
{value || '—'}
) const getLiveTiming = (mode: 'nip44' | 'nip04', type: 'encrypt' | 'decrypt') => { const timing = liveTiming[mode] if (timing && timing.type === type) { const elapsed = Math.round(performance.now() - timing.startTime) return elapsed } return null } const getBookmarkLiveTiming = (operation: 'loadBookmarks' | 'decryptBookmarks' | 'loadHighlights') => { const timing = liveTiming[operation] if (timing) { const elapsed = Math.round(performance.now() - timing.startTime) return elapsed } return null } const Stat = ({ label, value, mode, type, bookmarkOp }: { label: string; value?: string | number | null; mode?: 'nip44' | 'nip04'; type?: 'encrypt' | 'decrypt'; bookmarkOp?: 'loadBookmarks' | 'decryptBookmarks' | 'loadHighlights'; }) => { const liveValue = bookmarkOp ? getBookmarkLiveTiming(bookmarkOp) : (mode && type ? getLiveTiming(mode, type) : null) const isLive = !!liveValue let displayValue: string if (isLive) { displayValue = '' } else if (value !== null && value !== undefined) { displayValue = `${value}ms` } else { displayValue = '—' } return ( {label}: {isLive ? ( ) : ( displayValue )} ) } const debugContent = (

Debug

Active pubkey: {pubkey || 'none'}
{/* Account Connection Section */}

{activeAccount ? activeAccount.type === 'extension' ? 'Browser Extension' : activeAccount.type === 'nostr-connect' ? 'Bunker Connection' : 'Account Connection' : 'Account Connection'}

{!activeAccount ? (
Connect to your bunker (Nostr Connect signer) to enable encryption/decryption testing
setBunkerUri(e.target.value)} disabled={isBunkerLoading} />
{bunkerError && (
{bunkerError}
)}
) : (
{activeAccount.type === 'extension' ? 'Connected via browser extension' : activeAccount.type === 'nostr-connect' ? 'Connected to bunker' : 'Connected'}
{pubkey}
)}
{/* Encryption Tools Section */}

Encryption Tools