diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx index 714880cc..46a4db62 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -1,28 +1,17 @@ -import React, { useState, useEffect, useMemo } from 'react' -import { useParams, useLocation, useNavigate } from 'react-router-dom' +import React, { useMemo } from 'react' +import { useParams, useLocation } from 'react-router-dom' import { Hooks } from 'applesauce-react' import { useEventStore } from 'applesauce-react/hooks' 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 { useArticleLoader } from '../hooks/useArticleLoader' import { useExternalUrlLoader } from '../hooks/useExternalUrlLoader' -import { loadContent, BookmarkReference } from '../utils/contentLoader' -import { HighlightVisibility } from './HighlightsPanel' -import { HighlightButton, HighlightButtonRef } from './HighlightButton' -import { createHighlight, eventToHighlight } from '../services/highlightCreationService' -import { useRef, useCallback } from 'react' -import { NostrEvent, nip19 } from 'nostr-tools' +import { useBookmarksData } from '../hooks/useBookmarksData' +import { useContentSelection } from '../hooks/useContentSelection' +import { useHighlightCreation } from '../hooks/useHighlightCreation' +import { useBookmarksUI } from '../hooks/useBookmarksUI' +import ThreePaneLayout from './ThreePaneLayout' + export type ViewMode = 'compact' | 'cards' | 'large' interface BookmarksProps { @@ -33,40 +22,14 @@ interface BookmarksProps { const Bookmarks: React.FC = ({ relayPool, onLogout }) => { const { naddr } = useParams<{ naddr?: string }>() const location = useLocation() - const navigate = useNavigate() - // Extract external URL from /r/* route const externalUrl = location.pathname.startsWith('/r/') - ? decodeURIComponent(location.pathname.slice(3)) // Remove '/r/' prefix and decode + ? decodeURIComponent(location.pathname.slice(3)) : undefined - const [bookmarks, setBookmarks] = useState([]) - const [bookmarksLoading, setBookmarksLoading] = useState(true) - const [highlights, setHighlights] = useState([]) - const [highlightsLoading, setHighlightsLoading] = useState(true) - const [selectedUrl, setSelectedUrl] = useState(undefined) - const [readerLoading, setReaderLoading] = useState(false) - const [readerContent, setReaderContent] = useState(undefined) - const [isCollapsed, setIsCollapsed] = useState(true) // Start collapsed - const [isHighlightsCollapsed, setIsHighlightsCollapsed] = useState(true) // Start collapsed - const [viewMode, setViewMode] = useState('compact') - const [showHighlights, setShowHighlights] = useState(true) - const [selectedHighlightId, setSelectedHighlightId] = useState(undefined) - const [showSettings, setShowSettings] = useState(false) - const [currentArticleCoordinate, setCurrentArticleCoordinate] = useState(undefined) - const [currentArticleEventId, setCurrentArticleEventId] = useState(undefined) - const [currentArticle, setCurrentArticle] = useState(undefined) // Store the current article event - const [highlightVisibility, setHighlightVisibility] = useState({ - nostrverse: true, - friends: true, - mine: true - }) - const [followedPubkeys, setFollowedPubkeys] = useState>(new Set()) - const [isRefreshing, setIsRefreshing] = useState(false) const activeAccount = Hooks.useActiveAccount() const accountManager = Hooks.useAccountManager() const eventStore = useEventStore() - const highlightButtonRef = useRef(null) const { settings, saveSettings, toastMessage, toastType, clearToast } = useSettings({ relayPool, @@ -75,6 +38,79 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { 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 useArticleLoader({ naddr, @@ -104,96 +140,6 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { 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 const classifiedHighlights = useMemo(() => { return highlights.map(h => { @@ -207,185 +153,56 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { }) }, [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 ( - <> -
-
- setIsCollapsed(!isCollapsed)} - onLogout={onLogout} - viewMode={viewMode} - onViewModeChange={setViewMode} - selectedUrl={selectedUrl} - onOpenSettings={() => { - setShowSettings(true) - setIsCollapsed(true) - setIsHighlightsCollapsed(true) - }} - onRefresh={handleRefreshBookmarks} - isRefreshing={isRefreshing} - loading={bookmarksLoading} - /> -
-
- {showSettings ? ( - setShowSettings(false)} - /> - ) : ( - { - setSelectedHighlightId(id) - if (isHighlightsCollapsed) setIsHighlightsCollapsed(false) - }} - selectedHighlightId={selectedHighlightId} - highlightVisibility={highlightVisibility} - onTextSelection={handleTextSelection} - onClearSelection={handleClearSelection} - currentUserPubkey={activeAccount?.pubkey} - followedPubkeys={followedPubkeys} - /> - )} -
-
- setIsHighlightsCollapsed(!isHighlightsCollapsed)} - onSelectUrl={handleSelectUrl} - selectedUrl={selectedUrl} - onToggleHighlights={setShowHighlights} - selectedHighlightId={selectedHighlightId} - onRefresh={handleFetchHighlights} - onHighlightClick={setSelectedHighlightId} - currentUserPubkey={activeAccount?.pubkey} - highlightVisibility={highlightVisibility} - onHighlightVisibilityChange={setHighlightVisibility} - followedPubkeys={followedPubkeys} - /> -
-
- {activeAccount && relayPool && ( - - )} - {toastMessage && ( - - )} - + setIsCollapsed(!isCollapsed)} + onLogout={onLogout} + onViewModeChange={setViewMode} + onOpenSettings={() => { + setShowSettings(true) + setIsCollapsed(true) + setIsHighlightsCollapsed(true) + }} + onRefresh={handleRefreshAll} + readerLoading={readerLoading} + readerContent={readerContent} + selectedUrl={selectedUrl} + settings={settings} + onSaveSettings={saveSettings} + onCloseSettings={() => setShowSettings(false)} + classifiedHighlights={classifiedHighlights} + showHighlights={showHighlights} + selectedHighlightId={selectedHighlightId} + highlightVisibility={highlightVisibility} + onHighlightClick={(id) => { + setSelectedHighlightId(id) + if (isHighlightsCollapsed) setIsHighlightsCollapsed(false) + }} + onTextSelection={handleTextSelection} + onClearSelection={handleClearSelection} + currentUserPubkey={activeAccount?.pubkey} + followedPubkeys={followedPubkeys} + highlights={highlights} + highlightsLoading={highlightsLoading} + onToggleHighlightsPanel={() => setIsHighlightsCollapsed(!isHighlightsCollapsed)} + onSelectUrl={handleSelectUrl} + onToggleHighlights={setShowHighlights} + onRefreshHighlights={handleFetchHighlights} + onHighlightVisibilityChange={setHighlightVisibility} + highlightButtonRef={highlightButtonRef} + onCreateHighlight={handleCreateHighlight} + hasActiveAccount={!!(activeAccount && relayPool)} + toastMessage={toastMessage ?? undefined} + toastType={toastType} + onClearToast={clearToast} + /> ) } diff --git a/src/components/ThreePaneLayout.tsx b/src/components/ThreePaneLayout.tsx new file mode 100644 index 00000000..5b0b021a --- /dev/null +++ b/src/components/ThreePaneLayout.tsx @@ -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 + onCloseSettings: () => void + classifiedHighlights: Highlight[] + showHighlights: boolean + selectedHighlightId?: string + highlightVisibility: HighlightVisibility + onHighlightClick: (id: string) => void + onTextSelection: (text: string) => void + onClearSelection: () => void + currentUserPubkey?: string + followedPubkeys: Set + + // 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 + onCreateHighlight: (text: string) => void + hasActiveAccount: boolean + + // Toast + toastMessage?: string + toastType?: 'success' | 'error' + onClearToast: () => void +} + +const ThreePaneLayout: React.FC = (props) => { + return ( + <> +
+
+ +
+
+ {props.showSettings ? ( + + ) : ( + + )} +
+
+ +
+
+ {props.hasActiveAccount && ( + + )} + {props.toastMessage && ( + + )} + + ) +} + +export default ThreePaneLayout + diff --git a/src/hooks/useBookmarksData.ts b/src/hooks/useBookmarksData.ts new file mode 100644 index 00000000..263e5ee5 --- /dev/null +++ b/src/hooks/useBookmarksData.ts @@ -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([]) + const [bookmarksLoading, setBookmarksLoading] = useState(true) + const [highlights, setHighlights] = useState([]) + const [highlightsLoading, setHighlightsLoading] = useState(true) + const [followedPubkeys, setFollowedPubkeys] = useState>(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 + } +} + diff --git a/src/hooks/useBookmarksUI.ts b/src/hooks/useBookmarksUI.ts new file mode 100644 index 00000000..91852a5c --- /dev/null +++ b/src/hooks/useBookmarksUI.ts @@ -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('compact') + const [showHighlights, setShowHighlights] = useState(true) + const [selectedHighlightId, setSelectedHighlightId] = useState(undefined) + const [showSettings, setShowSettings] = useState(false) + const [currentArticleCoordinate, setCurrentArticleCoordinate] = useState(undefined) + const [currentArticleEventId, setCurrentArticleEventId] = useState(undefined) + const [currentArticle, setCurrentArticle] = useState(undefined) + const [highlightVisibility, setHighlightVisibility] = useState({ + 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 + } +} + diff --git a/src/hooks/useContentSelection.ts b/src/hooks/useContentSelection.ts new file mode 100644 index 00000000..d6e15bec --- /dev/null +++ b/src/hooks/useContentSelection.ts @@ -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(undefined) + const [readerLoading, setReaderLoading] = useState(false) + const [readerContent, setReaderContent] = useState(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 + } +} + diff --git a/src/hooks/useHighlightCreation.ts b/src/hooks/useHighlightCreation.ts new file mode 100644 index 00000000..f85975c4 --- /dev/null +++ b/src/hooks/useHighlightCreation.ts @@ -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(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 + } +} +