Fix authors not loading from cache on startup
**Motivations:** - Authors were not being loaded from cache, causing 'Aucun contenu trouvé' message even when authors exist - useArticles only loaded articles from Nostr subscription, not authors from cache - Authors should be loaded from cache first (cache-first architecture) **Root causes:** - useArticles hook only subscribed to articles from Nostr, not loading authors from cache - No method to get all authors from cache - Authors were only extracted from articles returned by subscription, which may not include author presentations **Correctifs:** - Added getAll method to objectCache to retrieve all objects of a type from cache - Modified useArticles to load authors from cache on startup before subscribing to Nostr - Authors are now loaded from cache and merged with articles from subscription - totalSponsoring is calculated for each author when loading from cache **Evolutions:** - Authors are now available immediately from cache on page load - Better user experience: no 'Aucun contenu trouvé' when authors exist in cache - Cache-first architecture: authors loaded from cache before Nostr subscription **Pages affectées:** - lib/objectCache.ts - hooks/useArticles.ts
This commit is contained in:
parent
52bd9492b7
commit
29cb20c614
@ -4,6 +4,8 @@ import type { Article } from '@/types/nostr'
|
|||||||
import { applyFiltersAndSort } from '@/lib/articleFiltering'
|
import { applyFiltersAndSort } from '@/lib/articleFiltering'
|
||||||
import type { ArticleFilters } from '@/components/ArticleFilters'
|
import type { ArticleFilters } from '@/components/ArticleFilters'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
import { objectCache } from '@/lib/objectCache'
|
||||||
|
import { getAuthorSponsoring } from '@/lib/sponsoring'
|
||||||
|
|
||||||
export function useArticles(searchQuery: string = '', filters: ArticleFilters | null = null): {
|
export function useArticles(searchQuery: string = '', filters: ArticleFilters | null = null): {
|
||||||
articles: Article[]
|
articles: Article[]
|
||||||
@ -21,6 +23,39 @@ export function useArticles(searchQuery: string = '', filters: ArticleFilters |
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
|
// Load authors from cache first
|
||||||
|
const loadAuthorsFromCache = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const cachedAuthors = await objectCache.getAll('author')
|
||||||
|
const authors = cachedAuthors as Article[]
|
||||||
|
|
||||||
|
// Calculate totalSponsoring for each author
|
||||||
|
const authorsWithSponsoring = await Promise.all(
|
||||||
|
authors.map(async (author) => {
|
||||||
|
if (author.isPresentation && author.pubkey) {
|
||||||
|
author.totalSponsoring = await getAuthorSponsoring(author.pubkey)
|
||||||
|
}
|
||||||
|
return author
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (authorsWithSponsoring.length > 0) {
|
||||||
|
setArticles((prev) => {
|
||||||
|
// Merge with existing articles, avoiding duplicates
|
||||||
|
const existingIds = new Set(prev.map((a) => a.id))
|
||||||
|
const newAuthors = authorsWithSponsoring.filter((a) => !existingIds.has(a.id))
|
||||||
|
const merged = [...prev, ...newAuthors].sort((a, b) => b.createdAt - a.createdAt)
|
||||||
|
hasArticlesRef.current = merged.length > 0
|
||||||
|
return merged
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading authors from cache:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadAuthorsFromCache()
|
||||||
|
|
||||||
const unsubscribe = nostrService.subscribeToArticles(
|
const unsubscribe = nostrService.subscribeToArticles(
|
||||||
(article) => {
|
(article) => {
|
||||||
setArticles((prev) => {
|
setArticles((prev) => {
|
||||||
|
|||||||
@ -17,7 +17,12 @@ export async function buildPresentationEvent(
|
|||||||
category: 'sciencefiction' | 'research' = 'sciencefiction',
|
category: 'sciencefiction' | 'research' = 'sciencefiction',
|
||||||
version: number = 0,
|
version: number = 0,
|
||||||
index: number = 0
|
index: number = 0
|
||||||
) {
|
): Promise<{
|
||||||
|
kind: 1
|
||||||
|
created_at: number
|
||||||
|
tags: string[][]
|
||||||
|
content: string
|
||||||
|
}> {
|
||||||
// Extract presentation and contentDescription from draft.content
|
// Extract presentation and contentDescription from draft.content
|
||||||
// Format: "${presentation}\n\n---\n\nDescription du contenu :\n${contentDescription}"
|
// Format: "${presentation}\n\n---\n\nDescription du contenu :\n${contentDescription}"
|
||||||
const separator = '\n\n---\n\nDescription du contenu :\n'
|
const separator = '\n\n---\n\nDescription du contenu :\n'
|
||||||
@ -243,7 +248,7 @@ export async function fetchAuthorPresentationFromPool(
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise<import('@/types/nostr').AuthorPresentationArticle | null>((resolve) => {
|
||||||
let resolved = false
|
let resolved = false
|
||||||
const relayUrl = getPrimaryRelaySync()
|
const relayUrl = getPrimaryRelaySync()
|
||||||
const { createSubscription } = require('@/types/nostr-tools-extended')
|
const { createSubscription } = require('@/types/nostr-tools-extended')
|
||||||
@ -251,7 +256,7 @@ export async function fetchAuthorPresentationFromPool(
|
|||||||
|
|
||||||
const events: Event[] = []
|
const events: Event[] = []
|
||||||
|
|
||||||
const finalize = async (value: import('@/types/nostr').AuthorPresentationArticle | null) => {
|
const finalize = async (value: import('@/types/nostr').AuthorPresentationArticle | null): Promise<void> => {
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -275,7 +280,7 @@ export async function fetchAuthorPresentationFromPool(
|
|||||||
resolve(value)
|
resolve(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
sub.on('event', (event: Event) => {
|
sub.on('event', (event: Event): void => {
|
||||||
// Collect all events first
|
// Collect all events first
|
||||||
const tags = extractTagsFromEvent(event)
|
const tags = extractTagsFromEvent(event)
|
||||||
if (tags.type === 'author' && !tags.hidden) {
|
if (tags.type === 'author' && !tags.hidden) {
|
||||||
@ -283,7 +288,7 @@ export async function fetchAuthorPresentationFromPool(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
sub.on('eose', async () => {
|
sub.on('eose', async (): Promise<void> => {
|
||||||
// Get the latest version from all collected events
|
// Get the latest version from all collected events
|
||||||
const latestEvent = getLatestVersion(events)
|
const latestEvent = getLatestVersion(events)
|
||||||
if (latestEvent) {
|
if (latestEvent) {
|
||||||
@ -295,7 +300,7 @@ export async function fetchAuthorPresentationFromPool(
|
|||||||
}
|
}
|
||||||
await finalize(null)
|
await finalize(null)
|
||||||
})
|
})
|
||||||
setTimeout(async () => {
|
setTimeout(async (): Promise<void> => {
|
||||||
// Get the latest version from all collected events
|
// Get the latest version from all collected events
|
||||||
const latestEvent = getLatestVersion(events)
|
const latestEvent = getLatestVersion(events)
|
||||||
if (latestEvent) {
|
if (latestEvent) {
|
||||||
|
|||||||
@ -3,7 +3,14 @@ import { getPrimaryRelaySync } from './config'
|
|||||||
import type { Event } from 'nostr-tools'
|
import type { Event } from 'nostr-tools'
|
||||||
import { createSubscription } from '@/types/nostr-tools-extended'
|
import { createSubscription } from '@/types/nostr-tools-extended'
|
||||||
|
|
||||||
export function createMessageVerificationFilters(messageEventId: string, authorPubkey: string, recipientPubkey: string, articleId: string) {
|
export function createMessageVerificationFilters(messageEventId: string, authorPubkey: string, recipientPubkey: string, articleId: string): Array<{
|
||||||
|
kinds: number[]
|
||||||
|
ids: string[]
|
||||||
|
authors: string[]
|
||||||
|
'#p': string[]
|
||||||
|
'#e': string[]
|
||||||
|
limit: number
|
||||||
|
}> {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
kinds: [4],
|
kinds: [4],
|
||||||
@ -42,11 +49,11 @@ export function setupMessageVerificationHandlers(
|
|||||||
finalize: (value: boolean) => void,
|
finalize: (value: boolean) => void,
|
||||||
isResolved: () => boolean
|
isResolved: () => boolean
|
||||||
): void {
|
): void {
|
||||||
sub.on('event', (event: Event) => {
|
sub.on('event', (event: Event): void => {
|
||||||
handleMessageVerificationEvent(event, articleId, recipientPubkey, authorPubkey, finalize)
|
handleMessageVerificationEvent(event, articleId, recipientPubkey, authorPubkey, finalize)
|
||||||
})
|
})
|
||||||
|
|
||||||
sub.on('eose', () => {
|
sub.on('eose', (): void => {
|
||||||
console.warn('Private message not found on relay after EOSE', {
|
console.warn('Private message not found on relay after EOSE', {
|
||||||
messageEventId,
|
messageEventId,
|
||||||
articleId,
|
articleId,
|
||||||
@ -75,7 +82,7 @@ function createMessageVerificationSubscription(
|
|||||||
authorPubkey: string,
|
authorPubkey: string,
|
||||||
recipientPubkey: string,
|
recipientPubkey: string,
|
||||||
articleId: string
|
articleId: string
|
||||||
) {
|
): ReturnType<typeof createSubscription> {
|
||||||
const filters = createMessageVerificationFilters(messageEventId, authorPubkey, recipientPubkey, articleId)
|
const filters = createMessageVerificationFilters(messageEventId, authorPubkey, recipientPubkey, articleId)
|
||||||
const relayUrl = getPrimaryRelaySync()
|
const relayUrl = getPrimaryRelaySync()
|
||||||
return createSubscription(pool, [relayUrl], filters)
|
return createSubscription(pool, [relayUrl], filters)
|
||||||
@ -88,10 +95,10 @@ function createVerificationPromise(
|
|||||||
recipientPubkey: string,
|
recipientPubkey: string,
|
||||||
authorPubkey: string
|
authorPubkey: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
let resolved = false
|
let resolved = false
|
||||||
|
|
||||||
const finalize = (value: boolean) => {
|
const finalize = (value: boolean): void => {
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -275,6 +275,42 @@ class ObjectCacheService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all objects of a type from cache (non-hidden only)
|
||||||
|
*/
|
||||||
|
async getAll(objectType: ObjectType): Promise<unknown[]> {
|
||||||
|
try {
|
||||||
|
const db = await this.initDB(objectType)
|
||||||
|
const transaction = db.transaction(['objects'], 'readonly')
|
||||||
|
const store = transaction.objectStore('objects')
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = store.openCursor()
|
||||||
|
const objects: unknown[] = []
|
||||||
|
|
||||||
|
request.onsuccess = (event: Event): void => {
|
||||||
|
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||||
|
if (cursor) {
|
||||||
|
const obj = cursor.value as CachedObject
|
||||||
|
if (!obj.hidden) {
|
||||||
|
objects.push(obj.parsed)
|
||||||
|
}
|
||||||
|
cursor.continue()
|
||||||
|
} else {
|
||||||
|
resolve(objects)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = (): void => {
|
||||||
|
reject(request.error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error retrieving all ${objectType} objects from cache:`, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear cache for an object type
|
* Clear cache for an object type
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user