import { useEffect, useMemo, useRef, useState } from 'react' import { nostrService } from '@/lib/nostr' import type { Article } from '@/types/nostr' import { applyFiltersAndSort } from '@/lib/articleFiltering' import type { ArticleFilters } from '@/components/ArticleFilters' import { t } from '@/lib/i18n' import { objectCache } from '@/lib/objectCache' import { getAuthorSponsoring } from '@/lib/sponsoring' export function useArticles(searchQuery: string = '', filters: ArticleFilters | null = null): { articles: Article[] allArticles: Article[] loading: boolean error: string | null loadArticleContent: (articleId: string, authorPubkey: string) => Promise
} { const [articles, setArticles] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const hasArticlesRef = useRef(false) useEffect(() => { setLoading(true) setError(null) // Load authors from cache first const loadAuthorsFromCache = async (): Promise => { try { const cachedAuthors = await objectCache.getAll('author') const authors = cachedAuthors as Article[] // Display authors immediately (with existing totalSponsoring if available) if (authors.length > 0) { setArticles((prev) => { // Merge with existing articles, avoiding duplicates const existingIds = new Set(prev.map((a) => a.id)) const newAuthors = authors.filter((a) => !existingIds.has(a.id)) const merged = [...prev, ...newAuthors].sort((a, b) => b.createdAt - a.createdAt) hasArticlesRef.current = merged.length > 0 return merged }) setLoading(false) // Calculate totalSponsoring asynchronously from cache (non-blocking) // Only update authors that don't have totalSponsoring yet const authorsNeedingSponsoring = authors.filter( (author) => author.isPresentation && author.pubkey && author.totalSponsoring === undefined ) if (authorsNeedingSponsoring.length > 0) { // Load sponsoring from cache in parallel (fast, no network) const sponsoringPromises = authorsNeedingSponsoring.map(async (author) => { if (author.pubkey) { const totalSponsoring = await getAuthorSponsoring(author.pubkey, true) return { authorId: author.id, totalSponsoring } } return null }) const sponsoringResults = await Promise.all(sponsoringPromises) // Update articles with sponsoring amounts setArticles((prev) => prev.map((article) => { const sponsoringResult = sponsoringResults.find((r) => r?.authorId === article.id) if (sponsoringResult && article.isPresentation) { return { ...article, totalSponsoring: sponsoringResult.totalSponsoring } } return article }) ) } return true } // Cache is empty - stop loading immediately, no network requests needed setLoading(false) hasArticlesRef.current = false return false } catch (error) { console.error('Error loading authors from cache:', error) setLoading(false) return false } } let unsubscribe: (() => void) | null = null let timeout: NodeJS.Timeout | null = null void loadAuthorsFromCache().then((hasCachedAuthors) => { // Only subscribe to network if cache is empty (to fetch new content) // If cache has authors, we can skip network subscription for faster load if (!hasCachedAuthors) { unsubscribe = nostrService.subscribeToArticles( (article) => { setArticles((prev) => { if (prev.some((a) => a.id === article.id)) { return prev } const next = [article, ...prev].sort((a, b) => b.createdAt - a.createdAt) hasArticlesRef.current = next.length > 0 return next }) setLoading(false) }, 50 ) // Shorter timeout if cache is empty (5 seconds instead of 10) timeout = setTimeout(() => { setLoading(false) if (!hasArticlesRef.current) { setError(t('common.error.noContent')) } }, 5000) } }) return () => { if (unsubscribe) { unsubscribe() } if (timeout) { clearTimeout(timeout) } } }, []) const loadArticleContent = async (articleId: string, authorPubkey: string): Promise
=> { try { const article = await nostrService.getArticleById(articleId) if (article) { // Try to decrypt article content using decryption key from private messages const decryptedContent = await nostrService.getDecryptedArticleContent(articleId, authorPubkey) if (decryptedContent) { setArticles((prev) => prev.map((a) => (a.id === articleId ? { ...a, content: decryptedContent, paid: true } : a) ) ) } return article } } catch (e) { console.error('Error loading article content:', e) setError(e instanceof Error ? e.message : 'Failed to load article') } return null } // Apply filters and sorting const filteredArticles = useMemo(() => { const effectiveFilters = filters ?? ({ authorPubkey: null, sortBy: 'newest', category: 'all', } as const) if (!filters && !searchQuery.trim()) { return articles } return applyFiltersAndSort(articles, searchQuery, effectiveFilters) }, [articles, searchQuery, filters]) return { articles: filteredArticles, allArticles: articles, // Return all articles for filters component loading, error, loadArticleContent, } }