import React, { useState, useEffect, useRef } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSpinner, faExclamationCircle, faHighlighter, faBookmark, faList, faThLarge, faImage, faPenToSquare } from '@fortawesome/free-solid-svg-icons' import { Hooks } from 'applesauce-react' 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 { extractUrlsFromContent } from '../services/bookmarkHelpers' import { getCachedMeData, setCachedMeData, updateCachedHighlights } from '../services/meCache' import { faBooks } from '../icons/customIcons' import { usePullToRefresh } from '../hooks/usePullToRefresh' import PullToRefreshIndicator from './PullToRefreshIndicator' 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 [error, setError] = useState(null) const [viewMode, setViewMode] = useState('cards') const meContainerRef = useRef(null) const [refreshTrigger, setRefreshTrigger] = useState(0) // Update local state when prop changes useEffect(() => { if (propActiveTab) { setActiveTab(propActiveTab) } }, [propActiveTab]) useEffect(() => { const loadData = async () => { if (!viewingPubkey) { setError(isOwnProfile ? 'Please log in to view your data' : 'Invalid profile') setLoading(false) return } try { setLoading(true) setError(null) // 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) setError('Failed to load data. Please try again.') } finally { setLoading(false) } } loadData() }, [relayPool, viewingPubkey, isOwnProfile, activeAccount, refreshTrigger]) // Pull-to-refresh const pullToRefreshState = usePullToRefresh(meContainerRef, { onRefresh: () => { setRefreshTrigger(prev => prev + 1) }, isRefreshing: loading }) 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}` } // Helper to check if a bookmark has either content or a URL (same logic as BookmarkList) const hasContentOrUrl = (ib: IndividualBookmark) => { const hasContent = ib.content && ib.content.trim().length > 0 let hasUrl = false if (ib.kind === 39701) { const dTag = ib.tags?.find((t: string[]) => t[0] === 'd')?.[1] hasUrl = !!dTag && dTag.trim().length > 0 } else { const urls = extractUrlsFromContent(ib.content || '') hasUrl = urls.length > 0 } if (ib.kind === 30023) return true return hasContent || hasUrl } 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 (same logic as BookmarkList) const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) .filter(hasContentOrUrl) .sort((a, b) => ((b.added_at || 0) - (a.added_at || 0)) || ((b.created_at || 0) - (a.created_at || 0))) // Only show full loading screen if we don't have any data yet const hasData = highlights.length > 0 || bookmarks.length > 0 || readArticles.length > 0 || writings.length > 0 if (loading && !hasData) { return (
) } if (error) { return (

{error}

) } const renderTabContent = () => { switch (activeTab) { case 'highlights': return highlights.length === 0 ? (

{isOwnProfile ? 'No highlights yet. Start highlighting content to see them here!' : 'No highlights yet. You should shame them on nostr!'}

) : (
{highlights.map((highlight) => ( ))}
) case 'reading-list': return allIndividualBookmarks.length === 0 ? (

No bookmarks yet. Bookmark articles to see them here!

) : (
{allIndividualBookmarks.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': return readArticles.length === 0 ? (

No read articles yet. Mark articles as read to see them here!

) : (
{readArticles.map((post) => ( ))}
) case 'writings': return writings.length === 0 ? (

No articles written yet. Publish your first article to see it here!

) : (
{writings.map((post) => ( ))}
) default: return null } } return (
{viewingPubkey && } {loading && hasData && (
)}
{isOwnProfile && ( <> )}
{renderTabContent()}
) } export default Me