lint fix wip

This commit is contained in:
Nicolas Cantu 2026-01-07 02:11:40 +01:00
parent 9f7a0e1527
commit d068b67deb
10 changed files with 403 additions and 96 deletions

View File

@ -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<void> {
const state = nostrAuthService.getState()
@ -20,7 +19,13 @@ async function updateCache(): Promise<void> {
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 {

View File

@ -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')

View File

@ -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<Locale>(getLocale())
useEffect(() => {
// Load saved locale from localStorage
const loadLocale = (): void => {
// Load saved locale from IndexedDB
const loadLocale = async (): Promise<void> => {
try {
if (typeof window !== 'undefined') {
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)
}
}
} catch (e) {
console.error('Error loading locale:', e)
}
}
loadLocale()
void loadLocale()
}, [])
const handleLocaleChange = (locale: Locale): void => {
const handleLocaleChange = async (locale: Locale): Promise<void> => {
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)
}

View File

@ -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<void> => {
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<void> => {
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)
}

View File

@ -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) {
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)
}
}
// 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) {
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)

View File

@ -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 <name>")
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) {

127
lib/localeStorage.ts Normal file
View File

@ -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<IDBDatabase> {
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<Locale | null> {
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<void> {
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<void> {
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()

View File

@ -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
}
/**

View File

@ -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)

View File

@ -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)
const loadLocale = async (): Promise<void> => {
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
</I18nProvider>
)
}