From d068b67deb96a2023609bb0310410626accfcb1f Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Wed, 7 Jan 2026 02:11:40 +0100 Subject: [PATCH] lint fix wip --- components/CacheUpdateManager.tsx | 9 +- components/KeyManagementManager.tsx | 9 +- components/LanguageSelector.tsx | 29 +++--- components/LanguageSettingsManager.tsx | 25 +++-- components/SyncProgressBar.tsx | 82 ++++++++++++---- lib/articlePublisher.ts | 114 ++++++++++++++++++---- lib/localeStorage.ts | 127 +++++++++++++++++++++++++ lib/paymentNotes.ts | 49 +++++++++- lib/writeService.ts | 7 +- pages/_app.tsx | 48 +++++----- 10 files changed, 403 insertions(+), 96 deletions(-) create mode 100644 lib/localeStorage.ts diff --git a/components/CacheUpdateManager.tsx b/components/CacheUpdateManager.tsx index a5fa16b..ba2dcaa 100644 --- a/components/CacheUpdateManager.tsx +++ b/components/CacheUpdateManager.tsx @@ -2,7 +2,6 @@ import { useState } from 'react' import { useRouter } from 'next/router' import { nostrAuthService } from '@/lib/nostrAuth' import { objectCache } from '@/lib/objectCache' -import { syncUserContentToCache } from '@/lib/userContentSync' async function updateCache(): Promise { const state = nostrAuthService.getState() @@ -20,7 +19,13 @@ async function updateCache(): Promise { objectCache.clear('review_tip'), ]) - await syncUserContentToCache(state.pubkey) + const { swClient } = await import('@/lib/swClient') + const isReady = await swClient.isReady() + if (isReady) { + await swClient.startUserSync(state.pubkey) + } else { + throw new Error('Service Worker is not ready') + } } function ErrorMessage({ error }: { error: string }): React.ReactElement { diff --git a/components/KeyManagementManager.tsx b/components/KeyManagementManager.tsx index ed58218..7fb4a2b 100644 --- a/components/KeyManagementManager.tsx +++ b/components/KeyManagementManager.tsx @@ -141,10 +141,13 @@ export function KeyManagementManager(): React.ReactElement { setShowImportForm(false) await loadKeys() - // Sync user content to IndexedDB cache + // Sync user content via Service Worker if (result.publicKey) { - const { syncUserContentToCache } = await import('@/lib/userContentSync') - void syncUserContentToCache(result.publicKey) + const { swClient } = await import('@/lib/swClient') + const isReady = await swClient.isReady() + if (isReady) { + void swClient.startUserSync(result.publicKey) + } } } catch (e) { const errorMessage = e instanceof Error ? e.message : t('settings.keyManagement.import.error.failed') diff --git a/components/LanguageSelector.tsx b/components/LanguageSelector.tsx index fe39e4c..339545d 100644 --- a/components/LanguageSelector.tsx +++ b/components/LanguageSelector.tsx @@ -1,7 +1,6 @@ import { useState, useEffect } from 'react' import { setLocale, getLocale, type Locale } from '@/lib/i18n' - -const LOCALE_STORAGE_KEY = 'zapwall-locale' +import { localeStorage } from '@/lib/localeStorage' interface LocaleButtonProps { locale: Locale @@ -30,30 +29,30 @@ export function LanguageSelector(): React.ReactElement { const [currentLocale, setCurrentLocale] = useState(getLocale()) useEffect(() => { - // Load saved locale from localStorage - const loadLocale = (): void => { + // Load saved locale from IndexedDB + const loadLocale = async (): Promise => { try { - if (typeof window !== 'undefined') { - const savedLocale = window.localStorage.getItem(LOCALE_STORAGE_KEY) as Locale | null - if (savedLocale && (savedLocale === 'fr' || savedLocale === 'en')) { - setLocale(savedLocale) - setCurrentLocale(savedLocale) - } + // Migrate from localStorage if needed + await localeStorage.migrateFromLocalStorage() + + // Load from IndexedDB + const savedLocale = await localeStorage.getLocale() + if (savedLocale) { + setLocale(savedLocale) + setCurrentLocale(savedLocale) } } catch (e) { console.error('Error loading locale:', e) } } - loadLocale() + void loadLocale() }, []) - const handleLocaleChange = (locale: Locale): void => { + const handleLocaleChange = async (locale: Locale): Promise => { setLocale(locale) setCurrentLocale(locale) try { - if (typeof window !== 'undefined') { - window.localStorage.setItem(LOCALE_STORAGE_KEY, locale) - } + await localeStorage.saveLocale(locale) } catch (e) { console.error('Error saving locale:', e) } diff --git a/components/LanguageSettingsManager.tsx b/components/LanguageSettingsManager.tsx index 9fd9c1a..a42fba7 100644 --- a/components/LanguageSettingsManager.tsx +++ b/components/LanguageSettingsManager.tsx @@ -1,8 +1,7 @@ import { useState, useEffect } from 'react' import { setLocale, getLocale, type Locale } from '@/lib/i18n' import { t } from '@/lib/i18n' - -const LOCALE_STORAGE_KEY = 'zapwall-locale' +import { localeStorage } from '@/lib/localeStorage' interface LocaleOptionProps { locale: Locale @@ -32,14 +31,14 @@ export function LanguageSettingsManager(): React.ReactElement { const [loading, setLoading] = useState(true) useEffect(() => { - const loadLocale = (): void => { + const loadLocale = async (): Promise => { try { - if (typeof window === 'undefined') { - setLoading(false) - return - } - const savedLocale = window.localStorage.getItem(LOCALE_STORAGE_KEY) as Locale | null - if (savedLocale && (savedLocale === 'fr' || savedLocale === 'en')) { + // Migrate from localStorage if needed + await localeStorage.migrateFromLocalStorage() + + // Load from IndexedDB + const savedLocale = await localeStorage.getLocale() + if (savedLocale) { setLocale(savedLocale) setCurrentLocale(savedLocale) } @@ -49,16 +48,14 @@ export function LanguageSettingsManager(): React.ReactElement { setLoading(false) } } - loadLocale() + void loadLocale() }, []) - const handleLocaleChange = (locale: Locale): void => { + const handleLocaleChange = async (locale: Locale): Promise => { setLocale(locale) setCurrentLocale(locale) try { - if (typeof window !== 'undefined') { - window.localStorage.setItem(LOCALE_STORAGE_KEY, locale) - } + await localeStorage.saveLocale(locale) } catch (e) { console.error('Error saving locale:', e) } diff --git a/components/SyncProgressBar.tsx b/components/SyncProgressBar.tsx index 4c93cba..8f19a23 100644 --- a/components/SyncProgressBar.tsx +++ b/components/SyncProgressBar.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' import { nostrAuthService } from '@/lib/nostrAuth' -import { syncUserContentToCache, type SyncProgress } from '@/lib/userContentSync' +import type { SyncProgress } from '@/lib/userContentSync' import { getLastSyncDate, setLastSyncDate as setLastSyncDateStorage, getCurrentTimestamp, calculateDaysBetween } from '@/lib/syncStorage' import { MIN_EVENT_DATE } from '@/lib/platformConfig' import { objectCache } from '@/lib/objectCache' @@ -94,15 +94,39 @@ export function SyncProgressBar(): React.ReactElement | null { setSyncProgress({ currentStep: 0, totalSteps: 6, completed: false }) try { - await syncUserContentToCache(connectionState.pubkey, (progress) => { - setSyncProgress(progress) - if (progress.completed) { - setIsSyncing(false) - void loadSyncStatus() + const { swClient } = await import('@/lib/swClient') + const isReady = await swClient.isReady() + if (isReady) { + await swClient.startUserSync(connectionState.pubkey) + // Progress is tracked via syncProgressManager + // Listen to syncProgressManager for updates + const { syncProgressManager } = await import('@/lib/syncProgressManager') + const checkProgress = (): void => { + const currentProgress = syncProgressManager.getProgress() + if (currentProgress) { + setSyncProgress(currentProgress) + if (currentProgress.completed) { + setIsSyncing(false) + void loadSyncStatus() + } + } } - }) - // Check if sync completed successfully (if it didn't, isSyncing should still be false) - setIsSyncing(false) + // Check progress periodically + const progressInterval = setInterval(() => { + checkProgress() + const currentProgress = syncProgressManager.getProgress() + if (currentProgress?.completed) { + clearInterval(progressInterval) + } + }, 500) + // Cleanup after 60 seconds max + setTimeout(() => { + clearInterval(progressInterval) + setIsSyncing(false) + }, 60000) + } else { + setIsSyncing(false) + } } catch (autoSyncError) { console.error('[SyncProgressBar] Error during auto-sync:', autoSyncError) setIsSyncing(false) @@ -141,15 +165,41 @@ export function SyncProgressBar(): React.ReactElement | null { // Reload sync status await loadSyncStatus() - // Start full resynchronization + // Start full resynchronization via Service Worker if (state.pubkey !== null) { - await syncUserContentToCache(state.pubkey, (progress) => { - setSyncProgress(progress) - if (progress.completed) { - setIsSyncing(false) - void loadSyncStatus() + const { swClient } = await import('@/lib/swClient') + const isReady = await swClient.isReady() + if (isReady) { + await swClient.startUserSync(state.pubkey) + // Progress is tracked via syncProgressManager + // Listen to syncProgressManager for updates + const { syncProgressManager } = await import('@/lib/syncProgressManager') + const checkProgress = (): void => { + const currentProgress = syncProgressManager.getProgress() + if (currentProgress) { + setSyncProgress(currentProgress) + if (currentProgress.completed) { + setIsSyncing(false) + void loadSyncStatus() + } + } } - }) + // Check progress periodically + const progressInterval = setInterval(() => { + checkProgress() + const currentProgress = syncProgressManager.getProgress() + if (currentProgress?.completed) { + clearInterval(progressInterval) + } + }, 500) + // Cleanup after 60 seconds max + setTimeout(() => { + clearInterval(progressInterval) + setIsSyncing(false) + }, 60000) + } else { + setIsSyncing(false) + } } } catch (resyncError) { console.error('Error resynchronizing:', resyncError) diff --git a/lib/articlePublisher.ts b/lib/articlePublisher.ts index 45c8c73..78ff201 100644 --- a/lib/articlePublisher.ts +++ b/lib/articlePublisher.ts @@ -5,6 +5,11 @@ import { buildPresentationEvent, sendEncryptedContent } from './articlePublisher import type { ArticleDraft, AuthorPresentationDraft, PublishedArticle } from './articlePublisherTypes' import { prepareAuthorKeys, isValidCategory, type PublishValidationResult } from './articlePublisherValidation' import { buildFailure, encryptAndPublish } from './articlePublisherPublish' +import { writeOrchestrator } from './writeOrchestrator' +import { finalizeEvent } from 'nostr-tools' +import { hexToBytes } from 'nostr-tools/utils' +import { generateAuthorHashId } from './hashIdGenerator' +import { buildObjectId } from './urlGenerator' export type { ArticleDraft, AuthorPresentationDraft, PublishedArticle } from './articlePublisherTypes' @@ -174,31 +179,100 @@ export class ArticlePublisher { // Extract author name from title (format: "Présentation de ") const authorName = draft.title.replace(/^Présentation de /, '').trim() ?? 'Auteur' - // Build event with hash-based ID - const eventTemplate = await buildPresentationEvent(draft, authorPubkey, authorName, 'sciencefiction') - const publishedEvent = await nostrService.publishEvent(eventTemplate) + // Extract presentation and contentDescription from draft.content + const separator = '\n\n---\n\nDescription du contenu :\n' + const separatorIndex = draft.content.indexOf(separator) + const presentation = separatorIndex !== -1 ? draft.content.substring(0, separatorIndex) : draft.presentation + let contentDescription = separatorIndex !== -1 ? draft.content.substring(separatorIndex + separator.length) : draft.contentDescription - if (!publishedEvent) { + // Remove Bitcoin address from contentDescription if present + if (contentDescription) { + contentDescription = contentDescription + .split('\n') + .filter((line) => !line.includes('Adresse Bitcoin mainnet (pour le sponsoring)')) + .join('\n') + .trim() + } + + const category = 'sciencefiction' + const version = 0 + const index = 0 + + // Generate hash ID + const hashId = await generateAuthorHashId({ + pubkey: authorPubkey, + authorName, + presentation, + contentDescription, + mainnetAddress: draft.mainnetAddress ?? undefined, + pictureUrl: draft.pictureUrl ?? undefined, + category, + }) + + const hash = hashId + const id = buildObjectId(hash, index, version) + + // Build parsed AuthorPresentationArticle object + const parsedAuthor: import('@/types/nostr').AuthorPresentationArticle = { + id, + hash, + version, + index, + pubkey: authorPubkey, + title: draft.title, + preview: draft.preview, + content: draft.content, + description: presentation, + contentDescription, + thumbnailUrl: draft.pictureUrl ?? '', + createdAt: Math.floor(Date.now() / 1000), + zapAmount: 0, + paid: true, + category: 'author-presentation', + isPresentation: true, + mainnetAddress: draft.mainnetAddress ?? '', + totalSponsoring: 0, + originalCategory: 'science-fiction', + ...(draft.pictureUrl ? { bannerUrl: draft.pictureUrl } : {}), + } + + // Build event template + const eventTemplate = await buildPresentationEvent(draft, authorPubkey, authorName, category, version, index) + + // Set private key in orchestrator + writeOrchestrator.setPrivateKey(authorPrivateKey) + + // Finalize event + const secretKey = hexToBytes(authorPrivateKey) + const event = finalizeEvent(eventTemplate, secretKey) + + // Get active relays + const { relaySessionManager } = await import('./relaySessionManager') + const activeRelays = await relaySessionManager.getActiveRelays() + const { getPrimaryRelay } = await import('./config') + const relays = activeRelays.length > 0 ? activeRelays : [await getPrimaryRelay()] + + // Publish via writeOrchestrator (parallel network + local write) + const result = await writeOrchestrator.writeAndPublish( + { + objectType: 'author', + hash, + event, + parsed: parsedAuthor, + version, + hidden: false, + index, + }, + relays + ) + + if (!result.success) { return buildFailure('Failed to publish presentation article') } - // Parse and cache the published presentation immediately with published: false - // The published status will be updated asynchronously by publishEvent - const { parsePresentationEvent } = await import('./articlePublisherHelpers') - const { extractTagsFromEvent } = await import('./nostrTagSystem') - const parsed = await parsePresentationEvent(publishedEvent) - if (parsed) { - const tags = extractTagsFromEvent(publishedEvent) - const { id: tagId, version: tagVersion, hidden: tagHidden } = tags - if (tagId) { - const { writeService } = await import('./writeService') - await writeService.writeObject('author', tagId, publishedEvent, parsed, tagVersion ?? 0, tagHidden ?? false, undefined, false) - } - } - return { - articleId: publishedEvent.id, - previewEventId: publishedEvent.id, + articleId: event.id, + previewEventId: event.id, success: true, } } catch (error) { diff --git a/lib/localeStorage.ts b/lib/localeStorage.ts new file mode 100644 index 0000000..306468d --- /dev/null +++ b/lib/localeStorage.ts @@ -0,0 +1,127 @@ +/** + * IndexedDB storage for locale preference + */ + +const DB_NAME = 'nostr_paywall_settings' +const DB_VERSION = 2 // Incremented to add locale store +const STORE_NAME = 'locale' + +export type Locale = 'fr' | 'en' + +class LocaleStorageService { + private db: IDBDatabase | null = null + + private async initDB(): Promise { + if (this.db) { + return this.db + } + + return new Promise((resolve, reject) => { + if (typeof window === 'undefined' || !window.indexedDB) { + reject(new Error('IndexedDB is not available')) + return + } + + const request = indexedDB.open(DB_NAME, DB_VERSION) + + request.onerror = () => { + reject(new Error(`Failed to open IndexedDB: ${request.error}`)) + } + + request.onsuccess = () => { + this.db = request.result + resolve(this.db) + } + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME, { keyPath: 'key' }) + } + } + }) + } + + /** + * Get locale from IndexedDB + */ + async getLocale(): Promise { + try { + const db = await this.initDB() + const transaction = db.transaction([STORE_NAME], 'readonly') + const store = transaction.objectStore(STORE_NAME) + + return new Promise((resolve, reject) => { + const request = store.get('locale') + + request.onsuccess = () => { + const result = request.result as { key: string; value: Locale } | undefined + const locale = result?.value + if (locale === 'fr' || locale === 'en') { + resolve(locale) + } else { + resolve(null) + } + } + + request.onerror = () => reject(request.error) + }) + } catch (error) { + console.error('Error retrieving locale from IndexedDB:', error) + return null + } + } + + /** + * Save locale to IndexedDB + */ + async saveLocale(locale: Locale): Promise { + try { + const db = await this.initDB() + const transaction = db.transaction([STORE_NAME], 'readwrite') + const store = transaction.objectStore(STORE_NAME) + + return new Promise((resolve, reject) => { + const request = store.put({ key: 'locale', value: locale }) + + request.onsuccess = () => { + resolve() + } + + request.onerror = () => reject(request.error) + }) + } catch (error) { + console.error('Error saving locale to IndexedDB:', error) + throw error + } + } + + /** + * Migrate locale from localStorage to IndexedDB if it exists + */ + async migrateFromLocalStorage(): Promise { + if (typeof window === 'undefined') { + return + } + + try { + const LOCALE_STORAGE_KEY = 'zapwall-locale' + const storedLocale = window.localStorage.getItem(LOCALE_STORAGE_KEY) as Locale | null + + if (storedLocale && (storedLocale === 'fr' || storedLocale === 'en')) { + // Check if already in IndexedDB + const existingLocale = await this.getLocale() + if (!existingLocale) { + // Migrate from localStorage to IndexedDB + await this.saveLocale(storedLocale) + // Remove from localStorage after successful migration + window.localStorage.removeItem(LOCALE_STORAGE_KEY) + } + } + } catch (error) { + console.error('Error migrating locale from localStorage:', error) + } + } +} + +export const localeStorage = new LocaleStorageService() diff --git a/lib/paymentNotes.ts b/lib/paymentNotes.ts index 148a08b..4dbf1ca 100644 --- a/lib/paymentNotes.ts +++ b/lib/paymentNotes.ts @@ -4,6 +4,8 @@ import { PLATFORM_SERVICE } from './platformConfig' import { generatePurchaseHashId, generateReviewTipHashId, generateSponsoringHashId } from './hashIdGenerator' import { buildObjectId } from './urlGenerator' import type { Event, EventTemplate } from 'nostr-tools' +import type { Purchase } from '@/types/nostr' +import { writeOrchestrator } from './writeOrchestrator' /** * Publish an explicit payment note (kind 1) for a purchase @@ -68,6 +70,21 @@ export async function publishPurchaseNote(params: { tags.push(['json', paymentJson]) + // Build parsed Purchase object + const parsedPurchase: Purchase = { + id, + hash: hashId, + version: 0, + index: 0, + payerPubkey: params.payerPubkey, + articleId: params.articleId, + authorPubkey: params.authorPubkey, + amount: params.amount, + paymentHash: params.paymentHash, + createdAt: Math.floor(Date.now() / 1000), + kindType: 'purchase', + } + const eventTemplate: EventTemplate = { kind: 1, created_at: Math.floor(Date.now() / 1000), @@ -76,7 +93,37 @@ export async function publishPurchaseNote(params: { } nostrService.setPrivateKey(params.payerPrivateKey) - return nostrService.publishEvent(eventTemplate) + writeOrchestrator.setPrivateKey(params.payerPrivateKey) + + // Finalize event + const secretKey = hexToBytes(params.payerPrivateKey) + const event = finalizeEvent(eventTemplate, secretKey) + + // Get active relays + const { relaySessionManager } = await import('./relaySessionManager') + const activeRelays = await relaySessionManager.getActiveRelays() + const { getPrimaryRelay } = await import('./config') + const relays = activeRelays.length > 0 ? activeRelays : [await getPrimaryRelay()] + + // Publish via writeOrchestrator (parallel network + local write) + const result = await writeOrchestrator.writeAndPublish( + { + objectType: 'purchase', + hash: hashId, + event, + parsed: parsedPurchase, + version: 0, + hidden: false, + index: 0, + }, + relays + ) + + if (!result.success) { + return null + } + + return event } /** diff --git a/lib/writeService.ts b/lib/writeService.ts index 84f5b7a..dc33719 100644 --- a/lib/writeService.ts +++ b/lib/writeService.ts @@ -237,7 +237,7 @@ class WriteService { data: { type, objectType, objectId, eventId, notificationData: data }, }) }) - } + } else { // Fallback: direct write const { notificationService } = await import('./notificationService') await notificationService.createNotification({ @@ -245,8 +245,9 @@ class WriteService { objectType, objectId, eventId, - data - ) + data, + }) + } } catch (error) { console.error('[WriteService] Error creating notification:', error) diff --git a/pages/_app.tsx b/pages/_app.tsx index 61fc2a0..24d3e94 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -9,31 +9,36 @@ import { swSyncHandler } from '@/lib/swSyncHandler' import { swClient } from '@/lib/swClient' function I18nProvider({ children }: { children: React.ReactNode }): React.ReactElement { - // Get saved locale from localStorage or default to French - const getInitialLocale = (): 'fr' | 'en' => { - if (typeof window === 'undefined') { - return 'fr' - } - try { - const savedLocale = window.localStorage.getItem('zapwall-locale') as 'fr' | 'en' | null - if (savedLocale === 'fr' || savedLocale === 'en') { - return savedLocale - } - } catch { - // Fallback to browser locale detection - } - // Try to detect browser locale - const browserLocale = navigator.language.split('-')[0] - return browserLocale === 'en' ? 'en' : 'fr' - } - const [initialLocale, setInitialLocale] = React.useState<'fr' | 'en'>('fr') const [localeLoaded, setLocaleLoaded] = React.useState(false) React.useEffect(() => { - const locale = getInitialLocale() - setInitialLocale(locale) - setLocaleLoaded(true) + const loadLocale = async (): Promise => { + try { + // Migrate from localStorage if needed + const { localeStorage } = await import('@/lib/localeStorage') + await localeStorage.migrateFromLocalStorage() + + // Load from IndexedDB + const savedLocale = await localeStorage.getLocale() + if (savedLocale) { + setInitialLocale(savedLocale) + setLocaleLoaded(true) + return + } + } catch { + // Fallback to browser locale detection + } + + // Fallback: Try to detect browser locale + if (typeof window !== 'undefined') { + const browserLocale = navigator.language.split('-')[0] + setInitialLocale(browserLocale === 'en' ? 'en' : 'fr') + } + setLocaleLoaded(true) + } + + void loadLocale() }, []) const { loaded } = useI18n(initialLocale) @@ -143,4 +148,3 @@ export default function App({ Component, pageProps }: AppProps): React.ReactElem ) } -