157 lines
5.2 KiB
TypeScript
157 lines
5.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)
|
|
|
|
useEffect(() => {
|
|
const loadAuthorsFromCache = async (): Promise<boolean> => {
|
|
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
|
|
const sponsoringByAuthorId = new Map<string, number>()
|
|
sponsoringResults.forEach((result) => {
|
|
if (result) {
|
|
sponsoringByAuthorId.set(result.authorId, result.totalSponsoring)
|
|
}
|
|
})
|
|
|
|
setArticles((prev) =>
|
|
prev.map((article) => {
|
|
const totalSponsoring = sponsoringByAuthorId.get(article.id)
|
|
if (totalSponsoring !== undefined && article.isPresentation) {
|
|
return { ...article, totalSponsoring }
|
|
}
|
|
return article
|
|
})
|
|
)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Cache is empty - stop loading immediately, no network requests needed
|
|
setLoading(false)
|
|
hasArticlesRef.current = false
|
|
return false
|
|
} catch (loadError) {
|
|
console.error('Error loading authors from cache:', loadError)
|
|
setLoading(false)
|
|
return false
|
|
}
|
|
}
|
|
|
|
const load = async (): Promise<void> => {
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
const hasCachedAuthors = await loadAuthorsFromCache()
|
|
if (!hasCachedAuthors) {
|
|
setError(t('common.error.noContent'))
|
|
}
|
|
}
|
|
|
|
void load()
|
|
|
|
return () => {
|
|
// No cleanup needed - no network subscription
|
|
}
|
|
}, [])
|
|
|
|
const loadArticleContent = async (articleId: string, authorPubkey: string): Promise<Article | null> => {
|
|
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,
|
|
}
|
|
}
|