refactor(bookmarks): split Bookmarks.tsx into smaller hooks and components

- Extract useBookmarksData hook for data fetching
- Extract useContentSelection hook for content selection logic
- Extract useHighlightCreation hook for highlight creation
- Extract useBookmarksUI hook for UI state management
- Create ThreePaneLayout component to reduce JSX complexity
- Reduce Bookmarks.tsx from 392 lines to 209 lines
This commit is contained in:
Gigi
2025-10-07 21:47:59 +01:00
parent eee7628096
commit 70a830fb66
6 changed files with 621 additions and 314 deletions

View File

@@ -1,28 +1,17 @@
import React, { useState, useEffect, useMemo } from 'react' import React, { useMemo } from 'react'
import { useParams, useLocation, useNavigate } from 'react-router-dom' import { useParams, useLocation } from 'react-router-dom'
import { Hooks } from 'applesauce-react' import { Hooks } from 'applesauce-react'
import { useEventStore } from 'applesauce-react/hooks' import { useEventStore } from 'applesauce-react/hooks'
import { RelayPool } from 'applesauce-relay' import { RelayPool } from 'applesauce-relay'
import { Bookmark } from '../types/bookmarks'
import { Highlight } from '../types/highlights'
import { BookmarkList } from './BookmarkList'
import { fetchBookmarks } from '../services/bookmarkService'
import { fetchHighlights, fetchHighlightsForArticle } from '../services/highlightService'
import { fetchContacts } from '../services/contactService'
import ContentPanel from './ContentPanel'
import { HighlightsPanel } from './HighlightsPanel'
import { ReadableContent } from '../services/readerService'
import Settings from './Settings'
import Toast from './Toast'
import { useSettings } from '../hooks/useSettings' import { useSettings } from '../hooks/useSettings'
import { useArticleLoader } from '../hooks/useArticleLoader' import { useArticleLoader } from '../hooks/useArticleLoader'
import { useExternalUrlLoader } from '../hooks/useExternalUrlLoader' import { useExternalUrlLoader } from '../hooks/useExternalUrlLoader'
import { loadContent, BookmarkReference } from '../utils/contentLoader' import { useBookmarksData } from '../hooks/useBookmarksData'
import { HighlightVisibility } from './HighlightsPanel' import { useContentSelection } from '../hooks/useContentSelection'
import { HighlightButton, HighlightButtonRef } from './HighlightButton' import { useHighlightCreation } from '../hooks/useHighlightCreation'
import { createHighlight, eventToHighlight } from '../services/highlightCreationService' import { useBookmarksUI } from '../hooks/useBookmarksUI'
import { useRef, useCallback } from 'react' import ThreePaneLayout from './ThreePaneLayout'
import { NostrEvent, nip19 } from 'nostr-tools'
export type ViewMode = 'compact' | 'cards' | 'large' export type ViewMode = 'compact' | 'cards' | 'large'
interface BookmarksProps { interface BookmarksProps {
@@ -33,40 +22,14 @@ interface BookmarksProps {
const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => { const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
const { naddr } = useParams<{ naddr?: string }>() const { naddr } = useParams<{ naddr?: string }>()
const location = useLocation() const location = useLocation()
const navigate = useNavigate()
// Extract external URL from /r/* route
const externalUrl = location.pathname.startsWith('/r/') const externalUrl = location.pathname.startsWith('/r/')
? decodeURIComponent(location.pathname.slice(3)) // Remove '/r/' prefix and decode ? decodeURIComponent(location.pathname.slice(3))
: undefined : undefined
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
const [bookmarksLoading, setBookmarksLoading] = useState(true)
const [highlights, setHighlights] = useState<Highlight[]>([])
const [highlightsLoading, setHighlightsLoading] = useState(true)
const [selectedUrl, setSelectedUrl] = useState<string | undefined>(undefined)
const [readerLoading, setReaderLoading] = useState(false)
const [readerContent, setReaderContent] = useState<ReadableContent | undefined>(undefined)
const [isCollapsed, setIsCollapsed] = useState(true) // Start collapsed
const [isHighlightsCollapsed, setIsHighlightsCollapsed] = useState(true) // Start collapsed
const [viewMode, setViewMode] = useState<ViewMode>('compact')
const [showHighlights, setShowHighlights] = useState(true)
const [selectedHighlightId, setSelectedHighlightId] = useState<string | undefined>(undefined)
const [showSettings, setShowSettings] = useState(false)
const [currentArticleCoordinate, setCurrentArticleCoordinate] = useState<string | undefined>(undefined)
const [currentArticleEventId, setCurrentArticleEventId] = useState<string | undefined>(undefined)
const [currentArticle, setCurrentArticle] = useState<NostrEvent | undefined>(undefined) // Store the current article event
const [highlightVisibility, setHighlightVisibility] = useState<HighlightVisibility>({
nostrverse: true,
friends: true,
mine: true
})
const [followedPubkeys, setFollowedPubkeys] = useState<Set<string>>(new Set())
const [isRefreshing, setIsRefreshing] = useState(false)
const activeAccount = Hooks.useActiveAccount() const activeAccount = Hooks.useActiveAccount()
const accountManager = Hooks.useAccountManager() const accountManager = Hooks.useAccountManager()
const eventStore = useEventStore() const eventStore = useEventStore()
const highlightButtonRef = useRef<HighlightButtonRef>(null)
const { settings, saveSettings, toastMessage, toastType, clearToast } = useSettings({ const { settings, saveSettings, toastMessage, toastType, clearToast } = useSettings({
relayPool, relayPool,
@@ -75,6 +38,79 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
accountManager accountManager
}) })
const {
isCollapsed,
setIsCollapsed,
isHighlightsCollapsed,
setIsHighlightsCollapsed,
viewMode,
setViewMode,
showHighlights,
setShowHighlights,
selectedHighlightId,
setSelectedHighlightId,
showSettings,
setShowSettings,
currentArticleCoordinate,
setCurrentArticleCoordinate,
currentArticleEventId,
setCurrentArticleEventId,
currentArticle,
setCurrentArticle,
highlightVisibility,
setHighlightVisibility
} = useBookmarksUI({ settings })
const {
bookmarks,
bookmarksLoading,
highlights,
setHighlights,
highlightsLoading,
setHighlightsLoading,
followedPubkeys,
isRefreshing,
handleFetchHighlights,
handleRefreshAll
} = useBookmarksData({
relayPool,
activeAccount,
accountManager,
naddr,
currentArticleCoordinate,
currentArticleEventId
})
const {
selectedUrl,
setSelectedUrl,
readerLoading,
setReaderLoading,
readerContent,
setReaderContent,
handleSelectUrl
} = useContentSelection({
relayPool,
settings,
setIsCollapsed,
setShowSettings,
setCurrentArticle
})
const {
highlightButtonRef,
handleTextSelection,
handleClearSelection,
handleCreateHighlight
} = useHighlightCreation({
activeAccount,
relayPool,
currentArticle,
selectedUrl,
readerContent,
onHighlightCreated: (highlight) => setHighlights(prev => [highlight, ...prev])
})
// Load nostr-native article if naddr is in URL // Load nostr-native article if naddr is in URL
useArticleLoader({ useArticleLoader({
naddr, naddr,
@@ -104,96 +140,6 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
setCurrentArticleEventId setCurrentArticleEventId
}) })
// Load initial data on login
useEffect(() => {
if (!relayPool || !activeAccount) return
handleFetchBookmarks()
// Avoid overwriting article-specific highlights during initial article load
// If an article is being viewed (naddr present), let useArticleLoader own the first highlights set
if (!naddr) {
handleFetchHighlights()
}
handleFetchContacts()
}, [relayPool, activeAccount?.pubkey])
const handleFetchContacts = async () => {
if (!relayPool || !activeAccount) return
const contacts = await fetchContacts(relayPool, activeAccount.pubkey)
setFollowedPubkeys(contacts)
}
// Apply UI settings
useEffect(() => {
if (settings.defaultViewMode) setViewMode(settings.defaultViewMode)
if (settings.showHighlights !== undefined) setShowHighlights(settings.showHighlights)
// Apply default highlight visibility settings
setHighlightVisibility({
nostrverse: settings.defaultHighlightVisibilityNostrverse !== false,
friends: settings.defaultHighlightVisibilityFriends !== false,
mine: settings.defaultHighlightVisibilityMine !== false
})
// Always start with both panels collapsed on initial load
// Don't apply saved collapse settings on initial load - let user control them
}, [settings])
const handleFetchBookmarks = async () => {
if (!relayPool || !activeAccount) return
setBookmarksLoading(true)
try {
const fullAccount = accountManager.getActive()
await fetchBookmarks(relayPool, fullAccount || activeAccount, setBookmarks)
} finally {
setBookmarksLoading(false)
}
}
const handleFetchHighlights = async () => {
if (!relayPool) return
setHighlightsLoading(true)
try {
// If we're viewing an article, fetch highlights for that article
if (currentArticleCoordinate) {
const highlightsList: Highlight[] = []
await fetchHighlightsForArticle(
relayPool,
currentArticleCoordinate,
currentArticleEventId,
(highlight) => {
// Render each highlight immediately as it arrives
highlightsList.push(highlight)
setHighlights([...highlightsList].sort((a, b) => b.created_at - a.created_at))
}
)
console.log(`🔄 Refreshed ${highlightsList.length} highlights for article`)
}
// Otherwise, if logged in, fetch user's own highlights
else if (activeAccount) {
const fetchedHighlights = await fetchHighlights(relayPool, activeAccount.pubkey)
setHighlights(fetchedHighlights)
}
} catch (err) {
console.error('Failed to fetch highlights:', err)
} finally {
setHighlightsLoading(false)
}
}
const handleRefreshBookmarks = async () => {
if (!relayPool || !activeAccount || isRefreshing) return
setIsRefreshing(true)
try {
await handleFetchBookmarks()
await handleFetchHighlights()
await handleFetchContacts()
} catch (err) {
console.error('Failed to refresh bookmarks:', err)
} finally {
setIsRefreshing(false)
}
}
// Classify highlights with levels based on user context // Classify highlights with levels based on user context
const classifiedHighlights = useMemo(() => { const classifiedHighlights = useMemo(() => {
return highlights.map(h => { return highlights.map(h => {
@@ -207,185 +153,56 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
}) })
}, [highlights, activeAccount?.pubkey, followedPubkeys]) }, [highlights, activeAccount?.pubkey, followedPubkeys])
const handleSelectUrl = async (url: string, bookmark?: BookmarkReference) => {
if (!relayPool) return
// Update the URL path based on content type
if (bookmark && bookmark.kind === 30023) {
// For nostr articles, navigate to /a/:naddr
const dTag = bookmark.tags.find(t => t[0] === 'd')?.[1] || ''
if (dTag && bookmark.pubkey) {
const pointer = {
identifier: dTag,
kind: 30023,
pubkey: bookmark.pubkey,
}
const naddr = nip19.naddrEncode(pointer)
navigate(`/a/${naddr}`)
}
} else if (url) {
// For external URLs, navigate to /r/:url (encoded to preserve special chars like //)
navigate(`/r/${encodeURIComponent(url)}`)
}
setSelectedUrl(url)
setReaderLoading(true)
setReaderContent(undefined)
setCurrentArticle(undefined) // Clear previous article
setShowSettings(false)
if (settings.collapseOnArticleOpen !== false) setIsCollapsed(true)
try {
const content = await loadContent(url, relayPool, bookmark)
setReaderContent(content)
// Note: currentArticle is set by useArticleLoader when loading Nostr articles
// For web bookmarks, there's no article event to set
} catch (err) {
console.warn('Failed to fetch content:', err)
} finally {
setReaderLoading(false)
}
}
const handleTextSelection = useCallback((text: string) => {
highlightButtonRef.current?.updateSelection(text)
}, [])
const handleClearSelection = useCallback(() => {
highlightButtonRef.current?.clearSelection()
}, [])
const handleCreateHighlight = useCallback(async (text: string) => {
if (!activeAccount || !relayPool) {
console.error('Missing requirements for highlight creation')
return
}
// Need either a nostr article or an external URL
if (!currentArticle && !selectedUrl) {
console.error('No source available for highlight creation')
return
}
try {
// Determine the source: prefer currentArticle (for nostr content), fallback to selectedUrl (for external URLs)
const source = currentArticle || selectedUrl!
// For context extraction, use article content or reader content
const contentForContext = currentArticle
? currentArticle.content
: readerContent?.markdown || readerContent?.html
// Create and publish the highlight
const signedEvent = await createHighlight(
text,
source,
activeAccount,
relayPool,
contentForContext
)
console.log('✅ Highlight created successfully!')
highlightButtonRef.current?.clearSelection()
// Immediately add the highlight to the UI (optimistic update)
const newHighlight = eventToHighlight(signedEvent)
setHighlights(prev => [newHighlight, ...prev])
} catch (error) {
console.error('Failed to create highlight:', error)
}
}, [activeAccount, relayPool, currentArticle, selectedUrl, readerContent])
return ( return (
<> <ThreePaneLayout
<div className={`three-pane ${isCollapsed ? 'sidebar-collapsed' : ''} ${isHighlightsCollapsed ? 'highlights-collapsed' : ''}`}>
<div className="pane sidebar">
<BookmarkList
bookmarks={bookmarks}
onSelectUrl={handleSelectUrl}
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
onToggleCollapse={() => setIsCollapsed(!isCollapsed)} isHighlightsCollapsed={isHighlightsCollapsed}
onLogout={onLogout} showSettings={showSettings}
bookmarks={bookmarks}
bookmarksLoading={bookmarksLoading}
viewMode={viewMode} viewMode={viewMode}
isRefreshing={isRefreshing}
onToggleSidebar={() => setIsCollapsed(!isCollapsed)}
onLogout={onLogout}
onViewModeChange={setViewMode} onViewModeChange={setViewMode}
selectedUrl={selectedUrl}
onOpenSettings={() => { onOpenSettings={() => {
setShowSettings(true) setShowSettings(true)
setIsCollapsed(true) setIsCollapsed(true)
setIsHighlightsCollapsed(true) setIsHighlightsCollapsed(true)
}} }}
onRefresh={handleRefreshBookmarks} onRefresh={handleRefreshAll}
isRefreshing={isRefreshing} readerLoading={readerLoading}
loading={bookmarksLoading} readerContent={readerContent}
/>
</div>
<div className="pane main">
{showSettings ? (
<Settings
settings={settings}
onSave={saveSettings}
onClose={() => setShowSettings(false)}
/>
) : (
<ContentPanel
loading={readerLoading}
title={readerContent?.title}
html={readerContent?.html}
markdown={readerContent?.markdown}
image={readerContent?.image}
selectedUrl={selectedUrl} selectedUrl={selectedUrl}
highlights={classifiedHighlights} settings={settings}
onSaveSettings={saveSettings}
onCloseSettings={() => setShowSettings(false)}
classifiedHighlights={classifiedHighlights}
showHighlights={showHighlights} showHighlights={showHighlights}
highlightStyle={settings.highlightStyle || 'marker'} selectedHighlightId={selectedHighlightId}
highlightColor={settings.highlightColor || '#ffff00'} highlightVisibility={highlightVisibility}
onHighlightClick={(id) => { onHighlightClick={(id) => {
setSelectedHighlightId(id) setSelectedHighlightId(id)
if (isHighlightsCollapsed) setIsHighlightsCollapsed(false) if (isHighlightsCollapsed) setIsHighlightsCollapsed(false)
}} }}
selectedHighlightId={selectedHighlightId}
highlightVisibility={highlightVisibility}
onTextSelection={handleTextSelection} onTextSelection={handleTextSelection}
onClearSelection={handleClearSelection} onClearSelection={handleClearSelection}
currentUserPubkey={activeAccount?.pubkey} currentUserPubkey={activeAccount?.pubkey}
followedPubkeys={followedPubkeys} followedPubkeys={followedPubkeys}
/>
)}
</div>
<div className="pane highlights">
<HighlightsPanel
highlights={highlights} highlights={highlights}
loading={highlightsLoading} highlightsLoading={highlightsLoading}
isCollapsed={isHighlightsCollapsed} onToggleHighlightsPanel={() => setIsHighlightsCollapsed(!isHighlightsCollapsed)}
onToggleCollapse={() => setIsHighlightsCollapsed(!isHighlightsCollapsed)}
onSelectUrl={handleSelectUrl} onSelectUrl={handleSelectUrl}
selectedUrl={selectedUrl}
onToggleHighlights={setShowHighlights} onToggleHighlights={setShowHighlights}
selectedHighlightId={selectedHighlightId} onRefreshHighlights={handleFetchHighlights}
onRefresh={handleFetchHighlights}
onHighlightClick={setSelectedHighlightId}
currentUserPubkey={activeAccount?.pubkey}
highlightVisibility={highlightVisibility}
onHighlightVisibilityChange={setHighlightVisibility} onHighlightVisibilityChange={setHighlightVisibility}
followedPubkeys={followedPubkeys} highlightButtonRef={highlightButtonRef}
onCreateHighlight={handleCreateHighlight}
hasActiveAccount={!!(activeAccount && relayPool)}
toastMessage={toastMessage ?? undefined}
toastType={toastType}
onClearToast={clearToast}
/> />
</div>
</div>
{activeAccount && relayPool && (
<HighlightButton
ref={highlightButtonRef}
onHighlight={handleCreateHighlight}
highlightColor={settings.highlightColor || '#ffff00'}
/>
)}
{toastMessage && (
<Toast
message={toastMessage}
type={toastType}
onClose={clearToast}
/>
)}
</>
) )
} }

View File

@@ -0,0 +1,158 @@
import React from 'react'
import { BookmarkList } from './BookmarkList'
import ContentPanel from './ContentPanel'
import { HighlightsPanel } from './HighlightsPanel'
import Settings from './Settings'
import Toast from './Toast'
import { HighlightButton } from './HighlightButton'
import { ViewMode } from './Bookmarks'
import { Bookmark } from '../types/bookmarks'
import { Highlight } from '../types/highlights'
import { ReadableContent } from '../services/readerService'
import { UserSettings } from '../services/settingsService'
import { HighlightVisibility } from './HighlightsPanel'
import { HighlightButtonRef } from './HighlightButton'
import { BookmarkReference } from '../utils/contentLoader'
interface ThreePaneLayoutProps {
// Layout state
isCollapsed: boolean
isHighlightsCollapsed: boolean
showSettings: boolean
// Bookmarks pane
bookmarks: Bookmark[]
bookmarksLoading: boolean
viewMode: ViewMode
isRefreshing: boolean
onToggleSidebar: () => void
onLogout: () => void
onViewModeChange: (mode: ViewMode) => void
onOpenSettings: () => void
onRefresh: () => void
// Content pane
readerLoading: boolean
readerContent?: ReadableContent
selectedUrl?: string
settings: UserSettings
onSaveSettings: (settings: UserSettings) => Promise<void>
onCloseSettings: () => void
classifiedHighlights: Highlight[]
showHighlights: boolean
selectedHighlightId?: string
highlightVisibility: HighlightVisibility
onHighlightClick: (id: string) => void
onTextSelection: (text: string) => void
onClearSelection: () => void
currentUserPubkey?: string
followedPubkeys: Set<string>
// Highlights pane
highlights: Highlight[]
highlightsLoading: boolean
onToggleHighlightsPanel: () => void
onSelectUrl: (url: string, bookmark?: BookmarkReference) => void
onToggleHighlights: (show: boolean) => void
onRefreshHighlights: () => void
onHighlightVisibilityChange: (visibility: HighlightVisibility) => void
// Highlight button
highlightButtonRef: React.RefObject<HighlightButtonRef>
onCreateHighlight: (text: string) => void
hasActiveAccount: boolean
// Toast
toastMessage?: string
toastType?: 'success' | 'error'
onClearToast: () => void
}
const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
return (
<>
<div className={`three-pane ${props.isCollapsed ? 'sidebar-collapsed' : ''} ${props.isHighlightsCollapsed ? 'highlights-collapsed' : ''}`}>
<div className="pane sidebar">
<BookmarkList
bookmarks={props.bookmarks}
onSelectUrl={props.onSelectUrl}
isCollapsed={props.isCollapsed}
onToggleCollapse={props.onToggleSidebar}
onLogout={props.onLogout}
viewMode={props.viewMode}
onViewModeChange={props.onViewModeChange}
selectedUrl={props.selectedUrl}
onOpenSettings={props.onOpenSettings}
onRefresh={props.onRefresh}
isRefreshing={props.isRefreshing}
loading={props.bookmarksLoading}
/>
</div>
<div className="pane main">
{props.showSettings ? (
<Settings
settings={props.settings}
onSave={props.onSaveSettings}
onClose={props.onCloseSettings}
/>
) : (
<ContentPanel
loading={props.readerLoading}
title={props.readerContent?.title}
html={props.readerContent?.html}
markdown={props.readerContent?.markdown}
image={props.readerContent?.image}
selectedUrl={props.selectedUrl}
highlights={props.classifiedHighlights}
showHighlights={props.showHighlights}
highlightStyle={props.settings.highlightStyle || 'marker'}
highlightColor={props.settings.highlightColor || '#ffff00'}
onHighlightClick={props.onHighlightClick}
selectedHighlightId={props.selectedHighlightId}
highlightVisibility={props.highlightVisibility}
onTextSelection={props.onTextSelection}
onClearSelection={props.onClearSelection}
currentUserPubkey={props.currentUserPubkey}
followedPubkeys={props.followedPubkeys}
/>
)}
</div>
<div className="pane highlights">
<HighlightsPanel
highlights={props.highlights}
loading={props.highlightsLoading}
isCollapsed={props.isHighlightsCollapsed}
onToggleCollapse={props.onToggleHighlightsPanel}
onSelectUrl={props.onSelectUrl}
selectedUrl={props.selectedUrl}
onToggleHighlights={props.onToggleHighlights}
selectedHighlightId={props.selectedHighlightId}
onRefresh={props.onRefreshHighlights}
onHighlightClick={props.onHighlightClick}
currentUserPubkey={props.currentUserPubkey}
highlightVisibility={props.highlightVisibility}
onHighlightVisibilityChange={props.onHighlightVisibilityChange}
followedPubkeys={props.followedPubkeys}
/>
</div>
</div>
{props.hasActiveAccount && (
<HighlightButton
ref={props.highlightButtonRef}
onHighlight={props.onCreateHighlight}
highlightColor={props.settings.highlightColor || '#ffff00'}
/>
)}
{props.toastMessage && (
<Toast
message={props.toastMessage}
type={props.toastType}
onClose={props.onClearToast}
/>
)}
</>
)
}
export default ThreePaneLayout

View File

@@ -0,0 +1,118 @@
import { useState, useEffect, useCallback } from 'react'
import { RelayPool } from 'applesauce-relay'
import { Bookmark } from '../types/bookmarks'
import { Highlight } from '../types/highlights'
import { fetchBookmarks } from '../services/bookmarkService'
import { fetchHighlights, fetchHighlightsForArticle } from '../services/highlightService'
import { fetchContacts } from '../services/contactService'
import { Account } from 'applesauce-core/account'
interface UseBookmarksDataParams {
relayPool: RelayPool | null
activeAccount: any
accountManager: any
naddr?: string
currentArticleCoordinate?: string
currentArticleEventId?: string
}
export const useBookmarksData = ({
relayPool,
activeAccount,
accountManager,
naddr,
currentArticleCoordinate,
currentArticleEventId
}: UseBookmarksDataParams) => {
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
const [bookmarksLoading, setBookmarksLoading] = useState(true)
const [highlights, setHighlights] = useState<Highlight[]>([])
const [highlightsLoading, setHighlightsLoading] = useState(true)
const [followedPubkeys, setFollowedPubkeys] = useState<Set<string>>(new Set())
const [isRefreshing, setIsRefreshing] = useState(false)
const handleFetchContacts = useCallback(async () => {
if (!relayPool || !activeAccount) return
const contacts = await fetchContacts(relayPool, activeAccount.pubkey)
setFollowedPubkeys(contacts)
}, [relayPool, activeAccount])
const handleFetchBookmarks = useCallback(async () => {
if (!relayPool || !activeAccount) return
setBookmarksLoading(true)
try {
const fullAccount = accountManager.getActive()
await fetchBookmarks(relayPool, fullAccount || activeAccount, setBookmarks)
} finally {
setBookmarksLoading(false)
}
}, [relayPool, activeAccount, accountManager])
const handleFetchHighlights = useCallback(async () => {
if (!relayPool) return
setHighlightsLoading(true)
try {
if (currentArticleCoordinate) {
const highlightsList: Highlight[] = []
await fetchHighlightsForArticle(
relayPool,
currentArticleCoordinate,
currentArticleEventId,
(highlight) => {
highlightsList.push(highlight)
setHighlights([...highlightsList].sort((a, b) => b.created_at - a.created_at))
}
)
console.log(`🔄 Refreshed ${highlightsList.length} highlights for article`)
} else if (activeAccount) {
const fetchedHighlights = await fetchHighlights(relayPool, activeAccount.pubkey)
setHighlights(fetchedHighlights)
}
} catch (err) {
console.error('Failed to fetch highlights:', err)
} finally {
setHighlightsLoading(false)
}
}, [relayPool, activeAccount, currentArticleCoordinate, currentArticleEventId])
const handleRefreshAll = useCallback(async () => {
if (!relayPool || !activeAccount || isRefreshing) return
setIsRefreshing(true)
try {
await handleFetchBookmarks()
await handleFetchHighlights()
await handleFetchContacts()
} catch (err) {
console.error('Failed to refresh data:', err)
} finally {
setIsRefreshing(false)
}
}, [relayPool, activeAccount, isRefreshing, handleFetchBookmarks, handleFetchHighlights, handleFetchContacts])
// Load initial data
useEffect(() => {
if (!relayPool || !activeAccount) return
handleFetchBookmarks()
if (!naddr) {
handleFetchHighlights()
}
handleFetchContacts()
}, [relayPool, activeAccount?.pubkey])
return {
bookmarks,
bookmarksLoading,
highlights,
setHighlights,
highlightsLoading,
setHighlightsLoading,
followedPubkeys,
isRefreshing,
handleFetchBookmarks,
handleFetchHighlights,
handleRefreshAll
}
}

View File

@@ -0,0 +1,61 @@
import { useState, useEffect } from 'react'
import { NostrEvent } from 'nostr-tools'
import { HighlightVisibility } from '../components/HighlightsPanel'
import { UserSettings } from '../services/settingsService'
import { ViewMode } from '../components/Bookmarks'
interface UseBookmarksUIParams {
settings: UserSettings
}
export const useBookmarksUI = ({ settings }: UseBookmarksUIParams) => {
const [isCollapsed, setIsCollapsed] = useState(true)
const [isHighlightsCollapsed, setIsHighlightsCollapsed] = useState(true)
const [viewMode, setViewMode] = useState<ViewMode>('compact')
const [showHighlights, setShowHighlights] = useState(true)
const [selectedHighlightId, setSelectedHighlightId] = useState<string | undefined>(undefined)
const [showSettings, setShowSettings] = useState(false)
const [currentArticleCoordinate, setCurrentArticleCoordinate] = useState<string | undefined>(undefined)
const [currentArticleEventId, setCurrentArticleEventId] = useState<string | undefined>(undefined)
const [currentArticle, setCurrentArticle] = useState<NostrEvent | undefined>(undefined)
const [highlightVisibility, setHighlightVisibility] = useState<HighlightVisibility>({
nostrverse: true,
friends: true,
mine: true
})
// Apply UI settings
useEffect(() => {
if (settings.defaultViewMode) setViewMode(settings.defaultViewMode)
if (settings.showHighlights !== undefined) setShowHighlights(settings.showHighlights)
setHighlightVisibility({
nostrverse: settings.defaultHighlightVisibilityNostrverse !== false,
friends: settings.defaultHighlightVisibilityFriends !== false,
mine: settings.defaultHighlightVisibilityMine !== false
})
}, [settings])
return {
isCollapsed,
setIsCollapsed,
isHighlightsCollapsed,
setIsHighlightsCollapsed,
viewMode,
setViewMode,
showHighlights,
setShowHighlights,
selectedHighlightId,
setSelectedHighlightId,
showSettings,
setShowSettings,
currentArticleCoordinate,
setCurrentArticleCoordinate,
currentArticleEventId,
setCurrentArticleEventId,
currentArticle,
setCurrentArticle,
highlightVisibility,
setHighlightVisibility
}
}

View File

@@ -0,0 +1,75 @@
import { useState, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import { RelayPool } from 'applesauce-relay'
import { NostrEvent, nip19 } from 'nostr-tools'
import { loadContent, BookmarkReference } from '../utils/contentLoader'
import { ReadableContent } from '../services/readerService'
import { UserSettings } from '../services/settingsService'
interface UseContentSelectionParams {
relayPool: RelayPool | null
settings: UserSettings
setIsCollapsed: (collapsed: boolean) => void
setShowSettings: (show: boolean) => void
setCurrentArticle: (article: NostrEvent | undefined) => void
}
export const useContentSelection = ({
relayPool,
settings,
setIsCollapsed,
setShowSettings,
setCurrentArticle
}: UseContentSelectionParams) => {
const navigate = useNavigate()
const [selectedUrl, setSelectedUrl] = useState<string | undefined>(undefined)
const [readerLoading, setReaderLoading] = useState(false)
const [readerContent, setReaderContent] = useState<ReadableContent | undefined>(undefined)
const handleSelectUrl = useCallback(async (url: string, bookmark?: BookmarkReference) => {
if (!relayPool) return
// Update the URL path based on content type
if (bookmark && bookmark.kind === 30023) {
const dTag = bookmark.tags.find(t => t[0] === 'd')?.[1] || ''
if (dTag && bookmark.pubkey) {
const pointer = {
identifier: dTag,
kind: 30023,
pubkey: bookmark.pubkey,
}
const naddr = nip19.naddrEncode(pointer)
navigate(`/a/${naddr}`)
}
} else if (url) {
navigate(`/r/${encodeURIComponent(url)}`)
}
setSelectedUrl(url)
setReaderLoading(true)
setReaderContent(undefined)
setCurrentArticle(undefined)
setShowSettings(false)
if (settings.collapseOnArticleOpen !== false) setIsCollapsed(true)
try {
const content = await loadContent(url, relayPool, bookmark)
setReaderContent(content)
} catch (err) {
console.warn('Failed to fetch content:', err)
} finally {
setReaderLoading(false)
}
}, [relayPool, settings, navigate, setIsCollapsed, setShowSettings, setCurrentArticle])
return {
selectedUrl,
setSelectedUrl,
readerLoading,
setReaderLoading,
readerContent,
setReaderContent,
handleSelectUrl
}
}

View File

@@ -0,0 +1,78 @@
import { useCallback, useRef } from 'react'
import { RelayPool } from 'applesauce-relay'
import { NostrEvent } from 'nostr-tools'
import { Highlight } from '../types/highlights'
import { ReadableContent } from '../services/readerService'
import { createHighlight, eventToHighlight } from '../services/highlightCreationService'
import { HighlightButtonRef } from '../components/HighlightButton'
interface UseHighlightCreationParams {
activeAccount: any
relayPool: RelayPool | null
currentArticle: NostrEvent | undefined
selectedUrl: string | undefined
readerContent: ReadableContent | undefined
onHighlightCreated: (highlight: Highlight) => void
}
export const useHighlightCreation = ({
activeAccount,
relayPool,
currentArticle,
selectedUrl,
readerContent,
onHighlightCreated
}: UseHighlightCreationParams) => {
const highlightButtonRef = useRef<HighlightButtonRef>(null)
const handleTextSelection = useCallback((text: string) => {
highlightButtonRef.current?.updateSelection(text)
}, [])
const handleClearSelection = useCallback(() => {
highlightButtonRef.current?.clearSelection()
}, [])
const handleCreateHighlight = useCallback(async (text: string) => {
if (!activeAccount || !relayPool) {
console.error('Missing requirements for highlight creation')
return
}
if (!currentArticle && !selectedUrl) {
console.error('No source available for highlight creation')
return
}
try {
const source = currentArticle || selectedUrl!
const contentForContext = currentArticle
? currentArticle.content
: readerContent?.markdown || readerContent?.html
const signedEvent = await createHighlight(
text,
source,
activeAccount,
relayPool,
contentForContext
)
console.log('✅ Highlight created successfully!')
highlightButtonRef.current?.clearSelection()
const newHighlight = eventToHighlight(signedEvent)
onHighlightCreated(newHighlight)
} catch (error) {
console.error('Failed to create highlight:', error)
}
}, [activeAccount, relayPool, currentArticle, selectedUrl, readerContent, onHighlightCreated])
return {
highlightButtonRef,
handleTextSelection,
handleClearSelection,
handleCreateHighlight
}
}