mirror of
https://github.com/dergigi/boris.git
synced 2025-12-19 07:34:28 +01:00
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:
@@ -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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
158
src/components/ThreePaneLayout.tsx
Normal file
158
src/components/ThreePaneLayout.tsx
Normal 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
|
||||||
|
|
||||||
118
src/hooks/useBookmarksData.ts
Normal file
118
src/hooks/useBookmarksData.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
61
src/hooks/useBookmarksUI.ts
Normal file
61
src/hooks/useBookmarksUI.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
75
src/hooks/useContentSelection.ts
Normal file
75
src/hooks/useContentSelection.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
78
src/hooks/useHighlightCreation.ts
Normal file
78
src/hooks/useHighlightCreation.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user