import React, { useState, useEffect } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSpinner, faHighlighter, faBookmark, faList, faThLarge, faImage, faPenToSquare } from '@fortawesome/free-solid-svg-icons' import { Hooks } from 'applesauce-react' import { BlogPostSkeleton, HighlightSkeleton, BookmarkSkeleton } from './Skeletons' import { RelayPool } from 'applesauce-relay' import { nip19 } from 'nostr-tools' import { useNavigate } from 'react-router-dom' import { Highlight } from '../types/highlights' import { HighlightItem } from './HighlightItem' import { fetchHighlights } from '../services/highlightService' import { fetchBookmarks } from '../services/bookmarkService' import { fetchReadArticlesWithData } from '../services/libraryService' import { BlogPostPreview, fetchBlogPostsFromAuthors } from '../services/exploreService' import { RELAYS } from '../config/relays' import { Bookmark, IndividualBookmark } from '../types/bookmarks' import AuthorCard from './AuthorCard' import BlogPostCard from './BlogPostCard' import { BookmarkItem } from './BookmarkItem' import IconButton from './IconButton' import { ViewMode } from './Bookmarks' import { getCachedMeData, setCachedMeData, updateCachedHighlights } from '../services/meCache' import { faBooks } from '../icons/customIcons' import { usePullToRefresh } from 'use-pull-to-refresh' import RefreshIndicator from './RefreshIndicator' import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils' interface MeProps { relayPool: RelayPool activeTab?: TabType pubkey?: string // Optional pubkey for viewing other users' profiles } type TabType = 'highlights' | 'reading-list' | 'archive' | 'writings' const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: propPubkey }) => { const activeAccount = Hooks.useActiveAccount() const navigate = useNavigate() const [activeTab, setActiveTab] = useState(propActiveTab || 'highlights') // Use provided pubkey or fall back to active account const viewingPubkey = propPubkey || activeAccount?.pubkey const isOwnProfile = !propPubkey || (activeAccount?.pubkey === propPubkey) const [highlights, setHighlights] = useState([]) const [bookmarks, setBookmarks] = useState([]) const [readArticles, setReadArticles] = useState([]) const [writings, setWritings] = useState([]) const [loading, setLoading] = useState(true) const [viewMode, setViewMode] = useState('cards') const [refreshTrigger, setRefreshTrigger] = useState(0) // Update local state when prop changes useEffect(() => { if (propActiveTab) { setActiveTab(propActiveTab) } }, [propActiveTab]) useEffect(() => { const loadData = async () => { if (!viewingPubkey) { setLoading(false) return } try { setLoading(true) // Seed from cache if available to avoid empty flash (own profile only) if (isOwnProfile) { const cached = getCachedMeData(viewingPubkey) if (cached) { setHighlights(cached.highlights) setBookmarks(cached.bookmarks) setReadArticles(cached.readArticles) } } // Fetch highlights and writings (public data) const [userHighlights, userWritings] = await Promise.all([ fetchHighlights(relayPool, viewingPubkey), fetchBlogPostsFromAuthors(relayPool, [viewingPubkey], RELAYS) ]) setHighlights(userHighlights) setWritings(userWritings) // Only fetch private data for own profile if (isOwnProfile && activeAccount) { const userReadArticles = await fetchReadArticlesWithData(relayPool, viewingPubkey) setReadArticles(userReadArticles) // Fetch bookmarks using callback pattern let fetchedBookmarks: Bookmark[] = [] try { await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => { fetchedBookmarks = newBookmarks setBookmarks(newBookmarks) }) } catch (err) { console.warn('Failed to load bookmarks:', err) setBookmarks([]) } // Update cache with all fetched data setCachedMeData(viewingPubkey, userHighlights, fetchedBookmarks, userReadArticles) } else { setBookmarks([]) setReadArticles([]) } } catch (err) { console.error('Failed to load data:', err) // No blocking error - user can pull-to-refresh } finally { setLoading(false) } } loadData() }, [relayPool, viewingPubkey, isOwnProfile, activeAccount, refreshTrigger]) // Pull-to-refresh const { isRefreshing, pullPosition } = usePullToRefresh({ onRefresh: () => { setRefreshTrigger(prev => prev + 1) }, maximumPullLength: 240, refreshThreshold: 80, isDisabled: !viewingPubkey }) const handleHighlightDelete = (highlightId: string) => { setHighlights(prev => { const updated = prev.filter(h => h.id !== highlightId) // Update cache when highlight is deleted (own profile only) if (isOwnProfile && viewingPubkey) { updateCachedHighlights(viewingPubkey, updated) } return updated }) } 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}` } const handleSelectUrl = (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => { if (bookmark && bookmark.kind === 30023) { // For kind:30023 articles, navigate to the article route 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 regular URLs, navigate to the reader route navigate(`/r/${encodeURIComponent(url)}`) } } // Merge and flatten all individual bookmarks const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) .filter(hasContent) const groups = groupIndividualBookmarks(allIndividualBookmarks) const sections: Array<{ key: string; title: string; items: IndividualBookmark[] }> = [ { key: 'private', title: 'Private bookmarks', items: groups.privateItems }, { key: 'public', title: 'Public bookmarks', items: groups.publicItems }, { key: 'web', title: 'Web bookmarks', items: groups.web }, { key: 'amethyst', title: 'Old Bookmarks (Legacy)', items: groups.amethyst } ] // Show content progressively - no blocking error screens const hasData = highlights.length > 0 || bookmarks.length > 0 || readArticles.length > 0 || writings.length > 0 const showSkeletons = loading && !hasData const renderTabContent = () => { switch (activeTab) { case 'highlights': if (showSkeletons) { return (
{Array.from({ length: 8 }).map((_, i) => ( ))}
) } return highlights.length === 0 ? (
) : (
{highlights.map((highlight) => ( ))}
) case 'reading-list': if (showSkeletons) { return (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) } return allIndividualBookmarks.length === 0 ? (
) : (
{sections.filter(s => s.items.length > 0).map(section => (

{section.title}

{section.items.map((individualBookmark, index) => ( ))}
))}
setViewMode('compact')} title="Compact list view" ariaLabel="Compact list view" variant={viewMode === 'compact' ? 'primary' : 'ghost'} /> setViewMode('cards')} title="Cards view" ariaLabel="Cards view" variant={viewMode === 'cards' ? 'primary' : 'ghost'} /> setViewMode('large')} title="Large preview view" ariaLabel="Large preview view" variant={viewMode === 'large' ? 'primary' : 'ghost'} />
) case 'archive': if (showSkeletons) { return (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) } return readArticles.length === 0 ? (
) : (
{readArticles.map((post) => ( ))}
) case 'writings': if (showSkeletons) { return (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) } return writings.length === 0 ? (
) : (
{writings.map((post) => ( ))}
) default: return null } } return (
{viewingPubkey && }
{isOwnProfile && ( <> )}
{renderTabContent()}
) } export default Me