story-research-zapwall/hooks/useArticles.ts
2026-01-10 10:50:47 +01:00

159 lines
6.2 KiB
TypeScript

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<Article | null>
} {
const [articles, setArticles] = useState<Article[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const hasArticlesRef = useRef(false)
useLoadAuthorsFromCache({ setArticles, setLoading, setError, hasArticlesRef })
const loadArticleContent = createLoadArticleContent({ setArticles, setError })
// Apply filters and sorting
const filteredArticles = useMemo(() => {
const effectiveFilters = filters ?? buildDefaultFilters()
if (!filters && !searchQuery.trim()) {
return articles
}
return applyFiltersAndSort(articles, searchQuery, effectiveFilters)
}, [articles, searchQuery, filters])
return { articles: filteredArticles, allArticles: articles, loading, error, loadArticleContent }
}
function buildDefaultFilters(): { authorPubkey: null; sortBy: 'newest'; category: 'all' } {
return { authorPubkey: null, sortBy: 'newest', category: 'all' }
}
function useLoadAuthorsFromCache(params: {
setArticles: (value: Article[] | ((prev: Article[]) => Article[])) => void
setLoading: (value: boolean) => void
setError: (value: string | null) => void
hasArticlesRef: { current: boolean }
}): void {
const { setArticles, setLoading, setError, hasArticlesRef } = params
useEffect(() => {
void loadInitialAuthors({ setArticles, setLoading, setError, hasArticlesRef })
}, [setArticles, setLoading, setError, hasArticlesRef])
}
async function loadInitialAuthors(params: {
setArticles: (value: Article[] | ((prev: Article[]) => Article[])) => void
setLoading: (value: boolean) => void
setError: (value: string | null) => void
hasArticlesRef: { current: boolean }
}): Promise<void> {
params.setLoading(true)
params.setError(null)
const hasCachedAuthors = await loadAuthorsFromCache(params)
if (!hasCachedAuthors) {
params.setError(t('common.error.noContent'))
}
}
async function loadAuthorsFromCache(params: {
setArticles: (value: Article[] | ((prev: Article[]) => Article[])) => void
setLoading: (value: boolean) => void
hasArticlesRef: { current: boolean }
}): Promise<boolean> {
try {
const authors = (await objectCache.getAll('author')) as Article[]
if (authors.length === 0) {
params.setLoading(false)
const {hasArticlesRef} = params
hasArticlesRef.current = false
return false
}
params.setArticles((prev) => {
const merged = mergeAuthorsIntoArticles({ prev, authors })
const {hasArticlesRef} = params
hasArticlesRef.current = merged.length > 0
return merged
})
params.setLoading(false)
void updateAuthorsSponsoringFromCache({ authors, setArticles: params.setArticles })
return true
} catch (loadError) {
console.error('Error loading authors from cache:', loadError)
params.setLoading(false)
return false
}
}
function mergeAuthorsIntoArticles(params: {
prev: Article[]
authors: Article[]
}): Article[] {
const existingIds = new Set(params.prev.map((a) => a.id))
const newAuthors = params.authors.filter((a) => !existingIds.has(a.id))
return [...params.prev, ...newAuthors].sort((a, b) => b.createdAt - a.createdAt)
}
async function updateAuthorsSponsoringFromCache(params: {
authors: Article[]
setArticles: (value: Article[] | ((prev: Article[]) => Article[])) => void
}): Promise<void> {
const authorsNeedingSponsoring = params.authors.filter((a) => a.isPresentation && a.pubkey && a.totalSponsoring === undefined)
if (authorsNeedingSponsoring.length === 0) {
return
}
const sponsoringByAuthorId = await loadSponsoringByAuthorId(authorsNeedingSponsoring)
params.setArticles((prev) => applySponsoringToArticles({ prev, sponsoringByAuthorId }))
}
async function loadSponsoringByAuthorId(authors: Article[]): Promise<Map<string, number>> {
const sponsoringResults = await Promise.all(authors.map((author) => loadAuthorSponsoring(author)))
return new Map(sponsoringResults.filter((r): r is { authorId: string; totalSponsoring: number } => Boolean(r)).map((r) => [r.authorId, r.totalSponsoring]))
}
async function loadAuthorSponsoring(author: Article): Promise<{ authorId: string; totalSponsoring: number } | null> {
if (!author.pubkey) {
return null
}
const totalSponsoring = await getAuthorSponsoring(author.pubkey, true)
return { authorId: author.id, totalSponsoring }
}
function applySponsoringToArticles(params: { prev: Article[]; sponsoringByAuthorId: Map<string, number> }): Article[] {
return params.prev.map((article) => {
const totalSponsoring = params.sponsoringByAuthorId.get(article.id)
return totalSponsoring !== undefined && article.isPresentation ? { ...article, totalSponsoring } : article
})
}
function createLoadArticleContent(params: {
setArticles: (value: Article[] | ((prev: Article[]) => Article[])) => void
setError: (value: string | null) => void
}): (articleId: string, authorPubkey: string) => Promise<Article | null> {
return async (articleId: string, authorPubkey: string): Promise<Article | null> => {
try {
const article = await nostrService.getArticleById(articleId)
if (!article) {
return null
}
const decryptedContent = await nostrService.getDecryptedArticleContent(articleId, authorPubkey)
if (decryptedContent) {
params.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)
params.setError(e instanceof Error ? e.message : 'Failed to load article')
return null
}
}
}