159 lines
6.2 KiB
TypeScript
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
|
|
}
|
|
}
|
|
}
|