mirror of
https://github.com/dergigi/boris.git
synced 2025-12-26 11:04:24 +01:00
- Create fetchHighlightsFromAuthors function for fetching highlights from multiple contacts - Add tab structure to Explore page (Writings and Highlights tabs) - Update explore cache to handle both blog posts and highlights - Add /explore/highlights route - Keep UI consistent with /me page tab structure - Implement pull-to-refresh for both tabs - Add proper caching and streaming for highlights
308 lines
9.5 KiB
TypeScript
308 lines
9.5 KiB
TypeScript
import React, { useMemo, useEffect, useRef } from 'react'
|
|
import { useParams, useLocation, useNavigate } from 'react-router-dom'
|
|
import { Hooks } from 'applesauce-react'
|
|
import { useEventStore } from 'applesauce-react/hooks'
|
|
import { RelayPool } from 'applesauce-relay'
|
|
import { nip19 } from 'nostr-tools'
|
|
import { useSettings } from '../hooks/useSettings'
|
|
import { useArticleLoader } from '../hooks/useArticleLoader'
|
|
import { useExternalUrlLoader } from '../hooks/useExternalUrlLoader'
|
|
import { useBookmarksData } from '../hooks/useBookmarksData'
|
|
import { useContentSelection } from '../hooks/useContentSelection'
|
|
import { useHighlightCreation } from '../hooks/useHighlightCreation'
|
|
import { useBookmarksUI } from '../hooks/useBookmarksUI'
|
|
import { useRelayStatus } from '../hooks/useRelayStatus'
|
|
import { useOfflineSync } from '../hooks/useOfflineSync'
|
|
import ThreePaneLayout from './ThreePaneLayout'
|
|
import Explore from './Explore'
|
|
import Me from './Me'
|
|
import { classifyHighlights } from '../utils/highlightClassification'
|
|
|
|
export type ViewMode = 'compact' | 'cards' | 'large'
|
|
|
|
interface BookmarksProps {
|
|
relayPool: RelayPool | null
|
|
onLogout: () => void
|
|
}
|
|
|
|
const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|
const { naddr, npub } = useParams<{ naddr?: string; npub?: string }>()
|
|
const location = useLocation()
|
|
const navigate = useNavigate()
|
|
const previousLocationRef = useRef<string>()
|
|
|
|
const externalUrl = location.pathname.startsWith('/r/')
|
|
? decodeURIComponent(location.pathname.slice(3))
|
|
: undefined
|
|
|
|
const showSettings = location.pathname === '/settings'
|
|
const showExplore = location.pathname.startsWith('/explore')
|
|
const showMe = location.pathname.startsWith('/me')
|
|
const showProfile = location.pathname.startsWith('/p/')
|
|
|
|
// Extract tab from explore routes
|
|
const exploreTab = location.pathname === '/explore/highlights' ? 'highlights' : 'writings'
|
|
|
|
// Extract tab from me routes
|
|
const meTab = location.pathname === '/me' ? 'highlights' :
|
|
location.pathname === '/me/highlights' ? 'highlights' :
|
|
location.pathname === '/me/reading-list' ? 'reading-list' :
|
|
location.pathname === '/me/archive' ? 'archive' :
|
|
location.pathname === '/me/writings' ? 'writings' : 'highlights'
|
|
|
|
// Extract tab from profile routes
|
|
const profileTab = location.pathname.endsWith('/writings') ? 'writings' : 'highlights'
|
|
|
|
// Decode npub to pubkey for profile view
|
|
let profilePubkey: string | undefined
|
|
if (npub && showProfile) {
|
|
try {
|
|
const decoded = nip19.decode(npub)
|
|
if (decoded.type === 'npub') {
|
|
profilePubkey = decoded.data
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to decode npub:', err)
|
|
}
|
|
}
|
|
|
|
// Track previous location for going back from settings/me/explore/profile
|
|
useEffect(() => {
|
|
if (!showSettings && !showMe && !showExplore && !showProfile) {
|
|
previousLocationRef.current = location.pathname
|
|
}
|
|
}, [location.pathname, showSettings, showMe, showExplore, showProfile])
|
|
|
|
const activeAccount = Hooks.useActiveAccount()
|
|
const accountManager = Hooks.useAccountManager()
|
|
const eventStore = useEventStore()
|
|
|
|
const { settings, saveSettings, toastMessage, toastType, clearToast } = useSettings({
|
|
relayPool,
|
|
eventStore,
|
|
pubkey: activeAccount?.pubkey,
|
|
accountManager
|
|
})
|
|
|
|
// Monitor relay status for offline sync
|
|
const relayStatuses = useRelayStatus({ relayPool })
|
|
|
|
// Automatically sync local events to remote relays when coming back online
|
|
useOfflineSync({
|
|
relayPool,
|
|
account: activeAccount || null,
|
|
eventStore,
|
|
relayStatuses,
|
|
enabled: true
|
|
})
|
|
|
|
const {
|
|
isMobile,
|
|
isSidebarOpen,
|
|
toggleSidebar,
|
|
isCollapsed,
|
|
setIsCollapsed,
|
|
isHighlightsCollapsed,
|
|
setIsHighlightsCollapsed,
|
|
viewMode,
|
|
setViewMode,
|
|
showHighlights,
|
|
setShowHighlights,
|
|
selectedHighlightId,
|
|
setSelectedHighlightId,
|
|
currentArticleCoordinate,
|
|
setCurrentArticleCoordinate,
|
|
currentArticleEventId,
|
|
setCurrentArticleEventId,
|
|
currentArticle,
|
|
setCurrentArticle,
|
|
highlightVisibility,
|
|
setHighlightVisibility
|
|
} = useBookmarksUI({ settings })
|
|
|
|
// Close sidebar on mobile when route changes (e.g., clicking on blog posts in Explore)
|
|
useEffect(() => {
|
|
if (isMobile && isSidebarOpen) {
|
|
toggleSidebar()
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [location.pathname])
|
|
|
|
const {
|
|
bookmarks,
|
|
bookmarksLoading,
|
|
highlights,
|
|
setHighlights,
|
|
highlightsLoading,
|
|
setHighlightsLoading,
|
|
followedPubkeys,
|
|
isRefreshing,
|
|
lastFetchTime,
|
|
handleFetchHighlights,
|
|
handleRefreshAll
|
|
} = useBookmarksData({
|
|
relayPool,
|
|
activeAccount,
|
|
accountManager,
|
|
naddr,
|
|
currentArticleCoordinate,
|
|
currentArticleEventId,
|
|
settings
|
|
})
|
|
|
|
const {
|
|
selectedUrl,
|
|
setSelectedUrl,
|
|
readerLoading,
|
|
setReaderLoading,
|
|
readerContent,
|
|
setReaderContent,
|
|
handleSelectUrl: baseHandleSelectUrl
|
|
} = useContentSelection({
|
|
relayPool,
|
|
settings,
|
|
setIsCollapsed,
|
|
setShowSettings: () => {}, // No-op since we use route-based settings now
|
|
setCurrentArticle
|
|
})
|
|
|
|
// Wrap handleSelectUrl to close mobile sidebar when selecting content
|
|
const handleSelectUrl = (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => {
|
|
if (isMobile && isSidebarOpen) {
|
|
toggleSidebar()
|
|
}
|
|
baseHandleSelectUrl(url, bookmark)
|
|
}
|
|
|
|
const {
|
|
highlightButtonRef,
|
|
handleTextSelection,
|
|
handleClearSelection,
|
|
handleCreateHighlight
|
|
} = useHighlightCreation({
|
|
activeAccount,
|
|
relayPool,
|
|
eventStore,
|
|
currentArticle,
|
|
selectedUrl,
|
|
readerContent,
|
|
onHighlightCreated: (highlight) => setHighlights(prev => [highlight, ...prev]),
|
|
settings
|
|
})
|
|
|
|
// Load nostr-native article if naddr is in URL
|
|
useArticleLoader({
|
|
naddr,
|
|
relayPool,
|
|
setSelectedUrl,
|
|
setReaderContent,
|
|
setReaderLoading,
|
|
setIsCollapsed,
|
|
setHighlights,
|
|
setHighlightsLoading,
|
|
setCurrentArticleCoordinate,
|
|
setCurrentArticleEventId,
|
|
setCurrentArticle,
|
|
settings
|
|
})
|
|
|
|
// Load external URL if /r/* route is used
|
|
useExternalUrlLoader({
|
|
url: externalUrl,
|
|
relayPool,
|
|
setSelectedUrl,
|
|
setReaderContent,
|
|
setReaderLoading,
|
|
setIsCollapsed,
|
|
setHighlights,
|
|
setHighlightsLoading,
|
|
setCurrentArticleCoordinate,
|
|
setCurrentArticleEventId
|
|
})
|
|
|
|
// Classify highlights with levels based on user context
|
|
const classifiedHighlights = useMemo(() => {
|
|
return classifyHighlights(highlights, activeAccount?.pubkey, followedPubkeys)
|
|
}, [highlights, activeAccount?.pubkey, followedPubkeys])
|
|
|
|
return (
|
|
<ThreePaneLayout
|
|
isCollapsed={isCollapsed}
|
|
isHighlightsCollapsed={isHighlightsCollapsed}
|
|
isSidebarOpen={isSidebarOpen}
|
|
showSettings={showSettings}
|
|
showExplore={showExplore}
|
|
showMe={showMe}
|
|
showProfile={showProfile}
|
|
bookmarks={bookmarks}
|
|
bookmarksLoading={bookmarksLoading}
|
|
viewMode={viewMode}
|
|
isRefreshing={isRefreshing}
|
|
lastFetchTime={lastFetchTime}
|
|
onToggleSidebar={isMobile ? toggleSidebar : () => setIsCollapsed(!isCollapsed)}
|
|
onLogout={onLogout}
|
|
onViewModeChange={setViewMode}
|
|
onOpenSettings={() => {
|
|
navigate('/settings')
|
|
if (isMobile) {
|
|
toggleSidebar()
|
|
} else {
|
|
setIsCollapsed(true)
|
|
}
|
|
setIsHighlightsCollapsed(true)
|
|
}}
|
|
onRefresh={handleRefreshAll}
|
|
relayPool={relayPool}
|
|
eventStore={eventStore}
|
|
readerLoading={readerLoading}
|
|
readerContent={readerContent}
|
|
selectedUrl={selectedUrl}
|
|
settings={settings}
|
|
onSaveSettings={saveSettings}
|
|
onCloseSettings={() => {
|
|
// Navigate back to previous location or default
|
|
const backTo = previousLocationRef.current || '/'
|
|
navigate(backTo)
|
|
}}
|
|
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}
|
|
activeAccount={activeAccount}
|
|
currentArticle={currentArticle}
|
|
highlights={highlights}
|
|
highlightsLoading={highlightsLoading}
|
|
onToggleHighlightsPanel={() => setIsHighlightsCollapsed(!isHighlightsCollapsed)}
|
|
onSelectUrl={handleSelectUrl}
|
|
onToggleHighlights={setShowHighlights}
|
|
onRefreshHighlights={handleFetchHighlights}
|
|
onHighlightVisibilityChange={setHighlightVisibility}
|
|
highlightButtonRef={highlightButtonRef}
|
|
onCreateHighlight={handleCreateHighlight}
|
|
hasActiveAccount={!!(activeAccount && relayPool)}
|
|
explore={showExplore ? (
|
|
relayPool ? <Explore relayPool={relayPool} activeTab={exploreTab} /> : null
|
|
) : undefined}
|
|
me={showMe ? (
|
|
relayPool ? <Me relayPool={relayPool} activeTab={meTab} /> : null
|
|
) : undefined}
|
|
profile={showProfile && profilePubkey ? (
|
|
relayPool ? <Me relayPool={relayPool} activeTab={profileTab} pubkey={profilePubkey} /> : null
|
|
) : undefined}
|
|
toastMessage={toastMessage ?? undefined}
|
|
toastType={toastType}
|
|
onClearToast={clearToast}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export default Bookmarks
|