lint fix wip
This commit is contained in:
parent
9f7a0e1527
commit
d068b67deb
@ -2,7 +2,6 @@ import { useState } from 'react'
|
|||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { nostrAuthService } from '@/lib/nostrAuth'
|
import { nostrAuthService } from '@/lib/nostrAuth'
|
||||||
import { objectCache } from '@/lib/objectCache'
|
import { objectCache } from '@/lib/objectCache'
|
||||||
import { syncUserContentToCache } from '@/lib/userContentSync'
|
|
||||||
|
|
||||||
async function updateCache(): Promise<void> {
|
async function updateCache(): Promise<void> {
|
||||||
const state = nostrAuthService.getState()
|
const state = nostrAuthService.getState()
|
||||||
@ -20,7 +19,13 @@ async function updateCache(): Promise<void> {
|
|||||||
objectCache.clear('review_tip'),
|
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 {
|
function ErrorMessage({ error }: { error: string }): React.ReactElement {
|
||||||
|
|||||||
@ -141,10 +141,13 @@ export function KeyManagementManager(): React.ReactElement {
|
|||||||
setShowImportForm(false)
|
setShowImportForm(false)
|
||||||
await loadKeys()
|
await loadKeys()
|
||||||
|
|
||||||
// Sync user content to IndexedDB cache
|
// Sync user content via Service Worker
|
||||||
if (result.publicKey) {
|
if (result.publicKey) {
|
||||||
const { syncUserContentToCache } = await import('@/lib/userContentSync')
|
const { swClient } = await import('@/lib/swClient')
|
||||||
void syncUserContentToCache(result.publicKey)
|
const isReady = await swClient.isReady()
|
||||||
|
if (isReady) {
|
||||||
|
void swClient.startUserSync(result.publicKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMessage = e instanceof Error ? e.message : t('settings.keyManagement.import.error.failed')
|
const errorMessage = e instanceof Error ? e.message : t('settings.keyManagement.import.error.failed')
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { setLocale, getLocale, type Locale } from '@/lib/i18n'
|
import { setLocale, getLocale, type Locale } from '@/lib/i18n'
|
||||||
|
import { localeStorage } from '@/lib/localeStorage'
|
||||||
const LOCALE_STORAGE_KEY = 'zapwall-locale'
|
|
||||||
|
|
||||||
interface LocaleButtonProps {
|
interface LocaleButtonProps {
|
||||||
locale: Locale
|
locale: Locale
|
||||||
@ -30,30 +29,30 @@ export function LanguageSelector(): React.ReactElement {
|
|||||||
const [currentLocale, setCurrentLocale] = useState<Locale>(getLocale())
|
const [currentLocale, setCurrentLocale] = useState<Locale>(getLocale())
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load saved locale from localStorage
|
// Load saved locale from IndexedDB
|
||||||
const loadLocale = (): void => {
|
const loadLocale = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (typeof window !== 'undefined') {
|
// Migrate from localStorage if needed
|
||||||
const savedLocale = window.localStorage.getItem(LOCALE_STORAGE_KEY) as Locale | null
|
await localeStorage.migrateFromLocalStorage()
|
||||||
if (savedLocale && (savedLocale === 'fr' || savedLocale === 'en')) {
|
|
||||||
|
// Load from IndexedDB
|
||||||
|
const savedLocale = await localeStorage.getLocale()
|
||||||
|
if (savedLocale) {
|
||||||
setLocale(savedLocale)
|
setLocale(savedLocale)
|
||||||
setCurrentLocale(savedLocale)
|
setCurrentLocale(savedLocale)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error loading locale:', e)
|
console.error('Error loading locale:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadLocale()
|
void loadLocale()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleLocaleChange = (locale: Locale): void => {
|
const handleLocaleChange = async (locale: Locale): Promise<void> => {
|
||||||
setLocale(locale)
|
setLocale(locale)
|
||||||
setCurrentLocale(locale)
|
setCurrentLocale(locale)
|
||||||
try {
|
try {
|
||||||
if (typeof window !== 'undefined') {
|
await localeStorage.saveLocale(locale)
|
||||||
window.localStorage.setItem(LOCALE_STORAGE_KEY, locale)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error saving locale:', e)
|
console.error('Error saving locale:', e)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { setLocale, getLocale, type Locale } from '@/lib/i18n'
|
import { setLocale, getLocale, type Locale } from '@/lib/i18n'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
import { localeStorage } from '@/lib/localeStorage'
|
||||||
const LOCALE_STORAGE_KEY = 'zapwall-locale'
|
|
||||||
|
|
||||||
interface LocaleOptionProps {
|
interface LocaleOptionProps {
|
||||||
locale: Locale
|
locale: Locale
|
||||||
@ -32,14 +31,14 @@ export function LanguageSettingsManager(): React.ReactElement {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadLocale = (): void => {
|
const loadLocale = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (typeof window === 'undefined') {
|
// Migrate from localStorage if needed
|
||||||
setLoading(false)
|
await localeStorage.migrateFromLocalStorage()
|
||||||
return
|
|
||||||
}
|
// Load from IndexedDB
|
||||||
const savedLocale = window.localStorage.getItem(LOCALE_STORAGE_KEY) as Locale | null
|
const savedLocale = await localeStorage.getLocale()
|
||||||
if (savedLocale && (savedLocale === 'fr' || savedLocale === 'en')) {
|
if (savedLocale) {
|
||||||
setLocale(savedLocale)
|
setLocale(savedLocale)
|
||||||
setCurrentLocale(savedLocale)
|
setCurrentLocale(savedLocale)
|
||||||
}
|
}
|
||||||
@ -49,16 +48,14 @@ export function LanguageSettingsManager(): React.ReactElement {
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadLocale()
|
void loadLocale()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleLocaleChange = (locale: Locale): void => {
|
const handleLocaleChange = async (locale: Locale): Promise<void> => {
|
||||||
setLocale(locale)
|
setLocale(locale)
|
||||||
setCurrentLocale(locale)
|
setCurrentLocale(locale)
|
||||||
try {
|
try {
|
||||||
if (typeof window !== 'undefined') {
|
await localeStorage.saveLocale(locale)
|
||||||
window.localStorage.setItem(LOCALE_STORAGE_KEY, locale)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error saving locale:', e)
|
console.error('Error saving locale:', e)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { nostrAuthService } from '@/lib/nostrAuth'
|
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 { getLastSyncDate, setLastSyncDate as setLastSyncDateStorage, getCurrentTimestamp, calculateDaysBetween } from '@/lib/syncStorage'
|
||||||
import { MIN_EVENT_DATE } from '@/lib/platformConfig'
|
import { MIN_EVENT_DATE } from '@/lib/platformConfig'
|
||||||
import { objectCache } from '@/lib/objectCache'
|
import { objectCache } from '@/lib/objectCache'
|
||||||
@ -94,15 +94,39 @@ export function SyncProgressBar(): React.ReactElement | null {
|
|||||||
setSyncProgress({ currentStep: 0, totalSteps: 6, completed: false })
|
setSyncProgress({ currentStep: 0, totalSteps: 6, completed: false })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await syncUserContentToCache(connectionState.pubkey, (progress) => {
|
const { swClient } = await import('@/lib/swClient')
|
||||||
setSyncProgress(progress)
|
const isReady = await swClient.isReady()
|
||||||
if (progress.completed) {
|
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)
|
setIsSyncing(false)
|
||||||
void loadSyncStatus()
|
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)
|
setIsSyncing(false)
|
||||||
|
}, 60000)
|
||||||
|
} else {
|
||||||
|
setIsSyncing(false)
|
||||||
|
}
|
||||||
} catch (autoSyncError) {
|
} catch (autoSyncError) {
|
||||||
console.error('[SyncProgressBar] Error during auto-sync:', autoSyncError)
|
console.error('[SyncProgressBar] Error during auto-sync:', autoSyncError)
|
||||||
setIsSyncing(false)
|
setIsSyncing(false)
|
||||||
@ -141,15 +165,41 @@ export function SyncProgressBar(): React.ReactElement | null {
|
|||||||
// Reload sync status
|
// Reload sync status
|
||||||
await loadSyncStatus()
|
await loadSyncStatus()
|
||||||
|
|
||||||
// Start full resynchronization
|
// Start full resynchronization via Service Worker
|
||||||
if (state.pubkey !== null) {
|
if (state.pubkey !== null) {
|
||||||
await syncUserContentToCache(state.pubkey, (progress) => {
|
const { swClient } = await import('@/lib/swClient')
|
||||||
setSyncProgress(progress)
|
const isReady = await swClient.isReady()
|
||||||
if (progress.completed) {
|
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)
|
setIsSyncing(false)
|
||||||
void loadSyncStatus()
|
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) {
|
} catch (resyncError) {
|
||||||
console.error('Error resynchronizing:', resyncError)
|
console.error('Error resynchronizing:', resyncError)
|
||||||
|
|||||||
@ -5,6 +5,11 @@ import { buildPresentationEvent, sendEncryptedContent } from './articlePublisher
|
|||||||
import type { ArticleDraft, AuthorPresentationDraft, PublishedArticle } from './articlePublisherTypes'
|
import type { ArticleDraft, AuthorPresentationDraft, PublishedArticle } from './articlePublisherTypes'
|
||||||
import { prepareAuthorKeys, isValidCategory, type PublishValidationResult } from './articlePublisherValidation'
|
import { prepareAuthorKeys, isValidCategory, type PublishValidationResult } from './articlePublisherValidation'
|
||||||
import { buildFailure, encryptAndPublish } from './articlePublisherPublish'
|
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'
|
export type { ArticleDraft, AuthorPresentationDraft, PublishedArticle } from './articlePublisherTypes'
|
||||||
|
|
||||||
@ -174,31 +179,100 @@ export class ArticlePublisher {
|
|||||||
// Extract author name from title (format: "Présentation de <name>")
|
// Extract author name from title (format: "Présentation de <name>")
|
||||||
const authorName = draft.title.replace(/^Présentation de /, '').trim() ?? 'Auteur'
|
const authorName = draft.title.replace(/^Présentation de /, '').trim() ?? 'Auteur'
|
||||||
|
|
||||||
// Build event with hash-based ID
|
// Extract presentation and contentDescription from draft.content
|
||||||
const eventTemplate = await buildPresentationEvent(draft, authorPubkey, authorName, 'sciencefiction')
|
const separator = '\n\n---\n\nDescription du contenu :\n'
|
||||||
const publishedEvent = await nostrService.publishEvent(eventTemplate)
|
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')
|
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 {
|
return {
|
||||||
articleId: publishedEvent.id,
|
articleId: event.id,
|
||||||
previewEventId: publishedEvent.id,
|
previewEventId: event.id,
|
||||||
success: true,
|
success: true,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
127
lib/localeStorage.ts
Normal file
127
lib/localeStorage.ts
Normal 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()
|
||||||
@ -4,6 +4,8 @@ import { PLATFORM_SERVICE } from './platformConfig'
|
|||||||
import { generatePurchaseHashId, generateReviewTipHashId, generateSponsoringHashId } from './hashIdGenerator'
|
import { generatePurchaseHashId, generateReviewTipHashId, generateSponsoringHashId } from './hashIdGenerator'
|
||||||
import { buildObjectId } from './urlGenerator'
|
import { buildObjectId } from './urlGenerator'
|
||||||
import type { Event, EventTemplate } from 'nostr-tools'
|
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
|
* Publish an explicit payment note (kind 1) for a purchase
|
||||||
@ -68,6 +70,21 @@ export async function publishPurchaseNote(params: {
|
|||||||
|
|
||||||
tags.push(['json', paymentJson])
|
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 = {
|
const eventTemplate: EventTemplate = {
|
||||||
kind: 1,
|
kind: 1,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
@ -76,7 +93,37 @@ export async function publishPurchaseNote(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nostrService.setPrivateKey(params.payerPrivateKey)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -237,7 +237,7 @@ class WriteService {
|
|||||||
data: { type, objectType, objectId, eventId, notificationData: data },
|
data: { type, objectType, objectId, eventId, notificationData: data },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
} else {
|
||||||
// Fallback: direct write
|
// Fallback: direct write
|
||||||
const { notificationService } = await import('./notificationService')
|
const { notificationService } = await import('./notificationService')
|
||||||
await notificationService.createNotification({
|
await notificationService.createNotification({
|
||||||
@ -245,8 +245,9 @@ class WriteService {
|
|||||||
objectType,
|
objectType,
|
||||||
objectId,
|
objectId,
|
||||||
eventId,
|
eventId,
|
||||||
data
|
data,
|
||||||
)
|
})
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[WriteService] Error creating notification:', error)
|
console.error('[WriteService] Error creating notification:', error)
|
||||||
|
|||||||
@ -9,31 +9,36 @@ import { swSyncHandler } from '@/lib/swSyncHandler'
|
|||||||
import { swClient } from '@/lib/swClient'
|
import { swClient } from '@/lib/swClient'
|
||||||
|
|
||||||
function I18nProvider({ children }: { children: React.ReactNode }): React.ReactElement {
|
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 [initialLocale, setInitialLocale] = React.useState<'fr' | 'en'>('fr')
|
||||||
const [localeLoaded, setLocaleLoaded] = React.useState(false)
|
const [localeLoaded, setLocaleLoaded] = React.useState(false)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const locale = getInitialLocale()
|
const loadLocale = async (): Promise<void> => {
|
||||||
setInitialLocale(locale)
|
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)
|
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)
|
const { loaded } = useI18n(initialLocale)
|
||||||
@ -143,4 +148,3 @@ export default function App({ Component, pageProps }: AppProps): React.ReactElem
|
|||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user