import React, { useState, useEffect, useCallback, useMemo } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faHighlighter, faPenToSquare } from '@fortawesome/free-solid-svg-icons' import { IEventStore } from 'applesauce-core' import { RelayPool } from 'applesauce-relay' import { nip19 } from 'nostr-tools' import { useNavigate } from 'react-router-dom' import { HighlightItem } from './HighlightItem' import { BlogPostPreview } from '../services/exploreService' import { KINDS } from '../config/kinds' import AuthorCard from './AuthorCard' import BlogPostCard from './BlogPostCard' import { BlogPostSkeleton, HighlightSkeleton } from './Skeletons' import { useStoreTimeline } from '../hooks/useStoreTimeline' import { eventToHighlight } from '../services/highlightEventProcessor' import { toBlogPostPreview } from '../utils/toBlogPostPreview' import { usePullToRefresh } from 'use-pull-to-refresh' import RefreshIndicator from './RefreshIndicator' import { Hooks } from 'applesauce-react' import { readingProgressController } from '../services/readingProgressController' import { writingsController } from '../services/writingsController' import { highlightsController } from '../services/highlightsController' interface ProfileProps { relayPool: RelayPool eventStore: IEventStore pubkey: string activeTab?: 'highlights' | 'writings' } const Profile: React.FC = ({ relayPool, eventStore, pubkey, activeTab: propActiveTab }) => { const navigate = useNavigate() const activeAccount = Hooks.useActiveAccount() const [activeTab, setActiveTab] = useState<'highlights' | 'writings'>(propActiveTab || 'highlights') const [refreshTrigger, setRefreshTrigger] = useState(0) // Reading progress state (naddr -> progress 0-1) const [readingProgressMap, setReadingProgressMap] = useState>(new Map()) // Load cached data from event store instantly const cachedHighlights = useStoreTimeline( eventStore, { kinds: [KINDS.Highlights], authors: [pubkey] }, eventToHighlight, [pubkey] ) const cachedWritings = useStoreTimeline( eventStore, { kinds: [30023], authors: [pubkey] }, toBlogPostPreview, [pubkey] ) // Sort writings by publication date, newest first const sortedWritings = useMemo(() => { return cachedWritings.slice().sort((a, b) => { const timeA = a.published || a.event.created_at const timeB = b.published || b.event.created_at return timeB - timeA }) }, [cachedWritings]) // Update local state when prop changes useEffect(() => { if (propActiveTab) { setActiveTab(propActiveTab) } }, [propActiveTab]) // Subscribe to reading progress controller useEffect(() => { // Get initial state immediately const initialMap = readingProgressController.getProgressMap() setReadingProgressMap(initialMap) // Subscribe to updates const unsubProgress = readingProgressController.onProgress((newMap) => { setReadingProgressMap(newMap) }) return () => { unsubProgress() } }, []) // Load reading progress data when logged in useEffect(() => { if (!activeAccount?.pubkey) { return } readingProgressController.start({ relayPool, eventStore, pubkey: activeAccount.pubkey, force: refreshTrigger > 0 }) }, [activeAccount?.pubkey, relayPool, eventStore, refreshTrigger]) // Background fetch via controllers to populate event store useEffect(() => { if (!pubkey || !relayPool || !eventStore) return // Start controllers to fetch and populate event store // Controllers handle streaming, deduplication, and storage highlightsController.start({ relayPool, eventStore, pubkey }) .catch(err => console.warn('⚠️ [Profile] Failed to fetch highlights:', err)) writingsController.start({ relayPool, eventStore, pubkey, force: refreshTrigger > 0 }) .catch(err => console.warn('⚠️ [Profile] Failed to fetch writings:', err)) }, [pubkey, relayPool, eventStore, refreshTrigger]) // Pull-to-refresh const { isRefreshing, pullPosition } = usePullToRefresh({ onRefresh: () => { setRefreshTrigger(prev => prev + 1) }, maximumPullLength: 240, refreshThreshold: 80, isDisabled: !pubkey }) const getPostUrl = (post: BlogPostPreview) => { const dTag = post.event.tags.find(t => t[0] === 'd')?.[1] || '' const naddr = nip19.naddrEncode({ kind: 30023, pubkey: post.author, identifier: dTag }) return `/a/${naddr}` } // Helper to get reading progress for a post const getReadingProgress = useCallback((post: BlogPostPreview): number | undefined => { const dTag = post.event.tags.find(t => t[0] === 'd')?.[1] if (!dTag) return undefined try { const naddr = nip19.naddrEncode({ kind: 30023, pubkey: post.author, identifier: dTag }) const progress = readingProgressMap.get(naddr) // Only log when found or map is empty if (progress || readingProgressMap.size === 0) { // Progress found or map is empty } return progress } catch (err) { return undefined } }, [readingProgressMap]) const handleHighlightDelete = () => { // Not allowed to delete other users' highlights return } const npub = nip19.npubEncode(pubkey) const showSkeletons = cachedHighlights.length === 0 && sortedWritings.length === 0 const renderTabContent = () => { switch (activeTab) { case 'highlights': if (showSkeletons) { return (
{Array.from({ length: 8 }).map((_, i) => ( ))}
) } return cachedHighlights.length === 0 ? (
No highlights yet.
) : (
{cachedHighlights.map((highlight) => ( ))}
) case 'writings': if (showSkeletons) { return (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) } return sortedWritings.length === 0 ? (
No articles written yet.
) : (
{sortedWritings.map((post) => ( ))}
) default: return null } } return (
{renderTabContent()}
) } export default Profile