import React, { useState, useEffect } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSpinner, faExclamationCircle, faNewspaper } from '@fortawesome/free-solid-svg-icons' import { Hooks } from 'applesauce-react' import { RelayPool } from 'applesauce-relay' import { nip19 } from 'nostr-tools' import { fetchContacts } from '../services/contactService' import { fetchBlogPostsFromAuthors, BlogPostPreview } from '../services/exploreService' import BlogPostCard from './BlogPostCard' import { getCachedPosts, upsertCachedPost, setCachedPosts } from '../services/exploreCache' interface ExploreProps { relayPool: RelayPool } const Explore: React.FC = ({ relayPool }) => { const activeAccount = Hooks.useActiveAccount() const [blogPosts, setBlogPosts] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { const loadBlogPosts = async () => { if (!activeAccount) { setError('Please log in to explore content from your friends') setLoading(false) return } try { // show spinner but keep existing posts setLoading(true) setError(null) // Seed from in-memory cache if available to avoid empty flash const cached = getCachedPosts(activeAccount.pubkey) if (cached && cached.length > 0 && blogPosts.length === 0) { setBlogPosts(cached) } // Fetch the user's contacts (friends) const contacts = await fetchContacts( relayPool, activeAccount.pubkey, (partial) => { // When local contacts are available, kick off early posts fetch if (partial.size > 0) { const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url) fetchBlogPostsFromAuthors( relayPool, Array.from(partial), relayUrls, (post) => { // merge into UI and cache as we stream setBlogPosts((prev) => { const exists = prev.some(p => p.event.id === post.event.id) if (exists) return prev const next = [...prev, post] return next.sort((a, b) => { const timeA = a.published || a.event.created_at const timeB = b.published || b.event.created_at return timeB - timeA }) }) setCachedPosts(activeAccount.pubkey, upsertCachedPost(activeAccount.pubkey, post)) } ).then((all) => { // Ensure union of streamed + final is displayed setBlogPosts((prev) => { const byId = new Map(prev.map(p => [p.event.id, p])) for (const post of all) byId.set(post.event.id, post) const merged = Array.from(byId.values()).sort((a, b) => { const timeA = a.published || a.event.created_at const timeB = b.published || b.event.created_at return timeB - timeA }) setCachedPosts(activeAccount.pubkey, merged) return merged }) }) } } ) if (contacts.size === 0) { setError('You are not following anyone yet. Follow some people to see their blog posts!') setLoading(false) return } // After full contacts, do a final pass for completeness const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url) const posts = await fetchBlogPostsFromAuthors(relayPool, Array.from(contacts), relayUrls) if (posts.length === 0) { setError('No blog posts found from your friends yet') } setBlogPosts((prev) => { const byId = new Map(prev.map(p => [p.event.id, p])) for (const post of posts) byId.set(post.event.id, post) const merged = Array.from(byId.values()).sort((a, b) => { const timeA = a.published || a.event.created_at const timeB = b.published || b.event.created_at return timeB - timeA }) setCachedPosts(activeAccount.pubkey, merged) return merged }) } catch (err) { console.error('Failed to load blog posts:', err) setError('Failed to load blog posts. Please try again.') } finally { setLoading(false) } } loadBlogPosts() }, [relayPool, activeAccount, blogPosts.length]) const getPostUrl = (post: BlogPostPreview) => { // Get the d-tag identifier const dTag = post.event.tags.find(t => t[0] === 'd')?.[1] || '' // Create naddr const naddr = nip19.naddrEncode({ kind: 30023, pubkey: post.author, identifier: dTag }) return `/a/${naddr}` } if (error) { return (

{error}

) } return (

Explore

Discover blog posts from your friends on Nostr

{loading && (
)}
{blogPosts.map((post) => ( t[0] === 'd')?.[1]}`} post={post} href={getPostUrl(post)} /> ))} {!loading && blogPosts.length === 0 && (

No blog posts found yet.

)}
) } export default Explore