import React, { useState, useEffect, useCallback } 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, fetchBlogPostsFromAuthors } from '../services/exploreService' import { fetchHighlights } from '../services/highlightService' import { RELAYS } from '../config/relays' 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' 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] ) // 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() console.log('[progress] 🎯 Profile: Initial progress map size:', initialMap.size) setReadingProgressMap(initialMap) // Subscribe to updates const unsubProgress = readingProgressController.onProgress((newMap) => { console.log('[progress] 🎯 Profile: Received progress update, size:', newMap.size) 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 to populate event store (non-blocking) useEffect(() => { if (!pubkey || !relayPool || !eventStore) return console.log('🔄 [Profile] Background fetching highlights and writings for', pubkey.slice(0, 8)) // Fetch highlights in background fetchHighlights(relayPool, pubkey, undefined, undefined, false, eventStore) .then(highlights => { console.log('✅ [Profile] Fetched', highlights.length, 'highlights') }) .catch(err => { console.warn('⚠️ [Profile] Failed to fetch highlights:', err) }) // Fetch writings in background (no limit for single user profile) fetchBlogPostsFromAuthors(relayPool, [pubkey], RELAYS, undefined, null) .then(writings => { writings.forEach(w => eventStore.add(w.event)) console.log('✅ [Profile] Fetched', writings.length, 'writings') }) .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) { console.log('[progress] 🔍 Profile lookup:', { title: post.title?.slice(0, 30), naddr: naddr.slice(0, 80), mapSize: readingProgressMap.size, mapKeys: readingProgressMap.size > 0 ? Array.from(readingProgressMap.keys()).slice(0, 3).map(k => k.slice(0, 80)) : [], progress: progress ? Math.round(progress * 100) + '%' : 'not found' }) } 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 && cachedWritings.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 cachedWritings.length === 0 ? (
No articles written yet.
) : (
{cachedWritings.map((post) => ( ))}
) default: return null } } return (
{renderTabContent()}
) } export default Profile