Files
boris/src/hooks/useBookmarksData.ts
Gigi c2223e6b08 feat: centralize bookmark loading with streaming and auto-decrypt
Implemented centralized bookmark loading system:
- Bookmarks loaded in App.tsx with streaming + auto-decrypt pattern
- Load triggers: login, app mount, manual refresh only
- No redundant fetching on route changes

Changes:
1. bookmarkService.ts: Refactored fetchBookmarks for streaming
   - Events stream with onEvent callback
   - Auto-decrypt encrypted content (NIP-04/NIP-44) as events arrive
   - Progressive UI updates during loading

2. App.tsx: Added centralized bookmark state
   - bookmarks and bookmarksLoading state in AppRoutes
   - loadBookmarks function with streaming support
   - Load on mount if account exists (app reopen)
   - Load when activeAccount changes (login)
   - handleRefreshBookmarks for manual refresh
   - Pass props to all Bookmarks components

3. Bookmarks.tsx: Accept bookmarks as props
   - Receive bookmarks, bookmarksLoading, onRefreshBookmarks
   - Pass onRefreshBookmarks to useBookmarksData

4. useBookmarksData.ts: Simplified to accept bookmarks as props
   - Removed bookmark fetching logic
   - Removed handleFetchBookmarks function
   - Accept onRefreshBookmarks callback
   - Use onRefreshBookmarks in handleRefreshAll

5. Me.tsx: Removed fallback bookmark loading
   - Removed fetchBookmarks import and calls
   - Use bookmarks directly from props (centralized source)

Benefits:
- Single source of truth for bookmarks
- No duplicate fetching across components
- Streaming + auto-decrypt for better UX
- Simpler, more maintainable code
- DRY principle: one place for bookmark loading
2025-10-17 22:06:33 +02:00

118 lines
4.1 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react'
import { RelayPool } from 'applesauce-relay'
import { IAccount } from 'applesauce-accounts'
import { Bookmark } from '../types/bookmarks'
import { Highlight } from '../types/highlights'
import { fetchHighlights, fetchHighlightsForArticle } from '../services/highlightService'
import { fetchContacts } from '../services/contactService'
import { UserSettings } from '../services/settingsService'
interface UseBookmarksDataParams {
relayPool: RelayPool | null
activeAccount: IAccount | undefined
naddr?: string
externalUrl?: string
currentArticleCoordinate?: string
currentArticleEventId?: string
settings?: UserSettings
bookmarks: Bookmark[] // Passed from App.tsx (centralized loading)
bookmarksLoading: boolean // Passed from App.tsx (centralized loading)
onRefreshBookmarks: () => Promise<void>
}
export const useBookmarksData = ({
relayPool,
activeAccount,
naddr,
externalUrl,
currentArticleCoordinate,
currentArticleEventId,
settings,
onRefreshBookmarks
}: Omit<UseBookmarksDataParams, 'bookmarks' | 'bookmarksLoading'>) => {
const [highlights, setHighlights] = useState<Highlight[]>([])
const [highlightsLoading, setHighlightsLoading] = useState(true)
const [followedPubkeys, setFollowedPubkeys] = useState<Set<string>>(new Set())
const [isRefreshing, setIsRefreshing] = useState(false)
const [lastFetchTime, setLastFetchTime] = useState<number | null>(null)
const handleFetchContacts = useCallback(async () => {
if (!relayPool || !activeAccount) return
const contacts = await fetchContacts(relayPool, activeAccount.pubkey)
setFollowedPubkeys(contacts)
}, [relayPool, activeAccount])
const handleFetchHighlights = useCallback(async () => {
if (!relayPool) return
setHighlightsLoading(true)
try {
if (currentArticleCoordinate) {
const highlightsMap = new Map<string, Highlight>()
await fetchHighlightsForArticle(
relayPool,
currentArticleCoordinate,
currentArticleEventId,
(highlight) => {
// Deduplicate highlights by ID as they arrive
if (!highlightsMap.has(highlight.id)) {
highlightsMap.set(highlight.id, highlight)
const highlightsList = Array.from(highlightsMap.values())
setHighlights(highlightsList.sort((a, b) => b.created_at - a.created_at))
}
},
settings
)
console.log(`🔄 Refreshed ${highlightsMap.size} highlights for article`)
} else if (activeAccount) {
const fetchedHighlights = await fetchHighlights(relayPool, activeAccount.pubkey, undefined, settings)
setHighlights(fetchedHighlights)
}
} catch (err) {
console.error('Failed to fetch highlights:', err)
} finally {
setHighlightsLoading(false)
}
}, [relayPool, activeAccount, currentArticleCoordinate, currentArticleEventId, settings])
const handleRefreshAll = useCallback(async () => {
if (!relayPool || !activeAccount || isRefreshing) return
setIsRefreshing(true)
try {
await onRefreshBookmarks()
await handleFetchHighlights()
await handleFetchContacts()
setLastFetchTime(Date.now())
} catch (err) {
console.error('Failed to refresh data:', err)
} finally {
setIsRefreshing(false)
}
}, [relayPool, activeAccount, isRefreshing, onRefreshBookmarks, handleFetchHighlights, handleFetchContacts])
// Fetch highlights/contacts independently
useEffect(() => {
if (!relayPool || !activeAccount) return
// Only fetch general highlights when not viewing an article (naddr) or external URL
// External URLs have their highlights fetched by useExternalUrlLoader
if (!naddr && !externalUrl) {
handleFetchHighlights()
}
handleFetchContacts()
}, [relayPool, activeAccount, naddr, externalUrl, handleFetchHighlights, handleFetchContacts])
return {
highlights,
setHighlights,
highlightsLoading,
setHighlightsLoading,
followedPubkeys,
isRefreshing,
lastFetchTime,
handleFetchHighlights,
handleRefreshAll
}
}