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, fetchBlogPostsFromAuthors } from '../services/exploreService' import { fetchHighlights } from '../services/highlightService' import { KINDS } from '../config/kinds' import { getActiveRelayUrls } from '../services/relayManager' 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] ) // 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 to populate event store (non-blocking) useEffect(() => { if (!pubkey || !relayPool || !eventStore) return // Fetch highlights in background fetchHighlights(relayPool, pubkey, undefined, undefined, false, eventStore) .then(() => { // Highlights fetched }) .catch(err => { console.warn('⚠️ [Profile] Failed to fetch highlights:', err) }) // Fetch writings in background (no limit for single user profile) fetchBlogPostsFromAuthors(relayPool, [pubkey], getActiveRelayUrls(relayPool), undefined, null) .then(writings => { writings.forEach(w => eventStore.add(w.event)) }) .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