lint fix wip

This commit is contained in:
Nicolas Cantu 2026-01-06 15:57:34 +01:00
parent 390f895920
commit 303c0bf7df
15 changed files with 340 additions and 30 deletions

View File

@ -202,7 +202,7 @@ const ArticleFieldsRight = ({
<MarkdownEditorTwoColumns
value={draft.content}
onChange={(value) => onDraftChange({ ...draft, content: value })}
pages={draft.pages}
{...(draft.pages ? { pages: draft.pages } : {})}
onPagesChange={(pages) => onDraftChange({ ...draft, pages })}
onMediaAdd={(media: MediaRef) => {
const nextMedia = [...(draft.media ?? []), media]

View File

@ -3,6 +3,7 @@ import { nostrAuthService } from '@/lib/nostrAuth'
import { keyManagementService } from '@/lib/keyManagement'
import { nip19 } from 'nostr-tools'
import { t } from '@/lib/i18n'
import { SyncProgressBar } from './SyncProgressBar'
interface PublicKeys {
publicKey: string
@ -251,6 +252,9 @@ export function KeyManagementManager(): React.ReactElement {
</div>
)}
{/* Sync Progress Bar */}
{publicKeys && <SyncProgressBar />}
{!publicKeys && !accountExists && (
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4 mb-6">
<p className="text-yellow-400 font-semibold mb-2">{t('settings.keyManagement.noAccount.title')}</p>

View File

@ -89,7 +89,7 @@ export function MarkdownEditorTwoColumns({
}}
uploading={uploading}
error={error}
onAddPage={onPagesChange ? handleAddPage : undefined}
{...(onPagesChange ? { onAddPage: handleAddPage } : {})}
/>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
@ -205,8 +205,8 @@ function PagesManager({
onContentChange={(content) => onPageContentChange(page.number, content)}
onTypeChange={(type) => onPageTypeChange(page.number, type)}
onRemove={() => onRemovePage(page.number)}
onImageUpload={(file) => {
void onImageUpload(file, page.number)
onImageUpload={async (file) => {
await onImageUpload(file, page.number)
}}
/>
))}

View File

@ -41,18 +41,18 @@ export function ReviewForm({ article, onSuccess, onCancel }: ReviewFormProps): R
return
}
const category = article.category ?? 'science-fiction'
const category = article.category === 'author-presentation' ? 'science-fiction' : (article.category ?? 'science-fiction')
const seriesId = article.seriesId ?? ''
await publishReview({
articleId: article.id,
seriesId,
category,
category: category === 'science-fiction' || category === 'scientific-research' ? category : 'science-fiction',
authorPubkey: article.pubkey,
reviewerPubkey: pubkey,
content: content.trim(),
title: title.trim() || undefined,
text: text.trim() || undefined,
...(title.trim() ? { title: title.trim() } : {}),
...(text.trim() ? { text: text.trim() } : {}),
authorPrivateKey: privateKey,
})

View File

@ -38,17 +38,17 @@ export function ReviewTipForm({ review, article, onSuccess, onCancel }: ReviewTi
}
const split = calculateReviewSplit()
const category = article.category === 'science-fiction' ? 'sciencefiction' : article.category === 'scientific-research' ? 'research' : 'sciencefiction'
// Build zap request tags
const category = article.category === 'author-presentation' ? undefined : (article.category === 'science-fiction' || article.category === 'scientific-research' ? article.category : undefined)
const zapRequestTags = buildReviewTipZapRequestTags({
articleId: article.id,
reviewId: review.id,
authorPubkey: article.pubkey,
reviewerPubkey: review.reviewerPubkey,
category: article.category,
seriesId: article.seriesId,
text: text.trim() || undefined,
...(category ? { category } : {}),
...(article.seriesId ? { seriesId: article.seriesId } : {}),
...(text.trim() ? { text: text.trim() } : {}),
})
// Create zap request event (kind 9734) and publish it

View File

@ -0,0 +1,160 @@
import { useState, useEffect } from 'react'
import { nostrAuthService } from '@/lib/nostrAuth'
import { syncUserContentToCache, type SyncProgress } from '@/lib/userContentSync'
import { getLastSyncDate, getCurrentTimestamp, calculateDaysBetween } from '@/lib/syncStorage'
import { MIN_EVENT_DATE } from '@/lib/platformConfig'
import { t } from '@/lib/i18n'
export function SyncProgressBar(): React.ReactElement | null {
const [syncProgress, setSyncProgress] = useState<SyncProgress | null>(null)
const [isSyncing, setIsSyncing] = useState(false)
const [lastSyncDate, setLastSyncDate] = useState<number | null>(null)
const [totalDays, setTotalDays] = useState<number>(0)
useEffect(() => {
void loadSyncStatus()
}, [])
async function loadSyncStatus(): Promise<void> {
try {
const state = nostrAuthService.getState()
if (!state.connected || !state.pubkey) {
return
}
const storedLastSyncDate = await getLastSyncDate()
const currentTimestamp = getCurrentTimestamp()
const days = calculateDaysBetween(storedLastSyncDate, currentTimestamp)
setLastSyncDate(storedLastSyncDate)
setTotalDays(days)
// If everything is synced (no days to sync), don't show the progress bar
if (days === 0 && storedLastSyncDate >= currentTimestamp) {
setSyncProgress(null)
}
} catch (error) {
console.error('Error loading sync status:', error)
}
}
async function startSync(): Promise<void> {
try {
const state = nostrAuthService.getState()
if (!state.connected || !state.pubkey) {
return
}
setIsSyncing(true)
setSyncProgress({ currentDay: 0, totalDays, completed: false })
await syncUserContentToCache(state.pubkey, (progress) => {
setSyncProgress(progress)
if (progress.completed) {
setIsSyncing(false)
void loadSyncStatus()
}
})
} catch (error) {
console.error('Error starting sync:', error)
setIsSyncing(false)
}
}
// Don't show if not connected or if everything is synced
const state = nostrAuthService.getState()
if (!state.connected || !state.pubkey) {
return null
}
// If everything is synced, don't show the progress bar
if (totalDays === 0 && lastSyncDate !== null && lastSyncDate >= getCurrentTimestamp()) {
return null
}
// If sync is completed and no days to sync, don't show
if (syncProgress?.completed && totalDays === 0) {
return null
}
const progressPercentage = syncProgress && totalDays > 0
? Math.min(100, (syncProgress.currentDay / totalDays) * 100)
: 0
const formatDate = (timestamp: number): string => {
const date = new Date(timestamp * 1000)
const locale = typeof window !== 'undefined' ? navigator.language : 'fr-FR'
return date.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric' })
}
const getStartDate = (): number => {
if (lastSyncDate !== null) {
return lastSyncDate
}
return MIN_EVENT_DATE
}
const startDate = getStartDate()
const endDate = getCurrentTimestamp()
return (
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-4 mt-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-neon-cyan">
{t('settings.sync.title')}
</h3>
{!isSyncing && totalDays > 0 && (
<button
onClick={() => {
void startSync()
}}
className="px-3 py-1 text-xs bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded border border-neon-cyan/50 hover:border-neon-cyan transition-colors"
>
{t('settings.sync.start')}
</button>
)}
</div>
{totalDays > 0 && (
<div className="mb-2">
<p className="text-sm text-cyber-accent">
{t('settings.sync.daysRange', {
startDate: formatDate(startDate),
endDate: formatDate(endDate),
days: totalDays,
})}
</p>
</div>
)}
{isSyncing && syncProgress && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-cyber-accent">
{t('settings.sync.progress', {
current: syncProgress.currentDay,
total: syncProgress.totalDays,
})}
</span>
<span className="text-neon-cyan font-semibold">
{Math.round(progressPercentage)}%
</span>
</div>
<div className="w-full bg-cyber-dark rounded-full h-2 overflow-hidden">
<div
className="bg-neon-cyan h-full transition-all duration-300"
style={{ width: `${progressPercentage}%` }}
/>
</div>
</div>
)}
{!isSyncing && totalDays === 0 && lastSyncDate !== null && (
<p className="text-sm text-green-400">
{t('settings.sync.completed')}
</p>
)}
</div>
)
}

View File

@ -1,5 +1,6 @@
import { useEffect, useState, type Dispatch, type SetStateAction } from 'react'
import type { Article } from '@/types/nostr'
import type { ArticleDraft } from '@/lib/articlePublisherTypes'
import { useArticleEditing } from '@/hooks/useArticleEditing'
import { UserArticlesView } from './UserArticlesList'
import { EditPanel } from './UserArticlesEditPanel'
@ -135,16 +136,29 @@ function buildUpdatedArticle(
pubkey: string,
newId: string
): Article {
const hash = newId.split('_')[0] ?? ''
const index = Number.parseInt(newId.split('_')[1] ?? '0', 10)
const version = Number.parseInt(newId.split('_')[2] ?? '0', 10)
return {
id: newId,
hash,
version,
index,
pubkey,
title: draft.title,
preview: draft.preview,
content: '',
description: draft.preview,
contentDescription: draft.preview,
createdAt: Math.floor(Date.now() / 1000),
zapAmount: draft.zapAmount,
paid: false,
thumbnailUrl: draft.bannerUrl ?? '',
...(draft.category ? { category: draft.category } : {}),
...(draft.seriesId ? { seriesId: draft.seriesId } : {}),
...(draft.bannerUrl ? { bannerUrl: draft.bannerUrl } : {}),
...(draft.media ? { media: draft.media } : {}),
...(draft.pages ? { pages: draft.pages } : {}),
}
}

View File

@ -4,6 +4,7 @@ import { storePrivateContent, getStoredPrivateContent } from './articleStorage'
import { buildTags } from './nostrTagSystem'
import { PLATFORM_SERVICE } from './platformConfig'
import { generateSeriesHashId, generatePublicationHashId } from './hashIdGenerator'
import { parseObjectId } from './urlGenerator'
import type { ArticleDraft, PublishedArticle } from './articlePublisher'
import type { AlbyInvoice } from '@/types/alby'
import type { Review, Series } from '@/types/nostr'
@ -64,12 +65,20 @@ export async function publishSeries(params: {
if (!published) {
throw new Error('Failed to publish series')
}
const parsed = parseObjectId(published.id)
const hash = parsed.hash ?? published.id
const version = parsed.version ?? 0
const index = parsed.index ?? 0
return {
id: published.id,
hash,
version,
index,
pubkey: params.authorPubkey,
title: params.title,
description: params.description,
preview: params.preview ?? params.description.substring(0, 200),
thumbnailUrl: params.coverUrl ?? '',
category,
...(params.coverUrl ? { coverUrl: params.coverUrl } : {}),
kindType: 'series',
@ -156,12 +165,20 @@ export async function publishReview(params: {
if (!published) {
throw new Error('Failed to publish review')
}
const parsed = parseObjectId(published.id)
const hash = parsed.hash ?? published.id
const version = parsed.version ?? 0
const index = parsed.index ?? 0
return {
id: published.id,
hash,
version,
index,
articleId: params.articleId,
authorPubkey: params.authorPubkey,
reviewerPubkey: params.reviewerPubkey,
content: params.content,
description: params.content.substring(0, 200),
createdAt: published.created_at,
...(params.title ? { title: params.title } : {}),
...(params.text ? { text: params.text } : {}),

View File

@ -181,8 +181,8 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
authorName: profileData?.authorName ?? '',
presentation: profileData?.presentation ?? '',
contentDescription: profileData?.contentDescription ?? '',
mainnetAddress: profileData?.mainnetAddress ?? tags.mainnetAddress,
pictureUrl: profileData?.pictureUrl ?? tags.pictureUrl,
mainnetAddress: profileData?.mainnetAddress ?? (typeof tags.mainnetAddress === 'string' ? tags.mainnetAddress : undefined),
pictureUrl: profileData?.pictureUrl ?? (typeof tags.pictureUrl === 'string' ? tags.pictureUrl : undefined),
category: profileData?.category ?? tags.category ?? 'sciencefiction',
})
}
@ -202,7 +202,7 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
content: event.content,
description: profileData?.presentation ?? tags.description ?? '', // Required field
contentDescription: profileData?.contentDescription ?? tags.description ?? '', // Required field
thumbnailUrl: (profileData?.pictureUrl ?? tags.pictureUrl) ?? '', // Required field
thumbnailUrl: (typeof profileData?.pictureUrl === 'string' ? profileData.pictureUrl : typeof tags.pictureUrl === 'string' ? tags.pictureUrl : ''), // Required field
createdAt: event.created_at,
zapAmount: 0,
paid: true,

View File

@ -1,5 +1,5 @@
import type { Event } from 'nostr-tools'
import type { Article, KindType, Purchase, Review, ReviewTip, Series, Sponsoring } from '@/types/nostr'
import type { Article, KindType, Page, Purchase, Review, ReviewTip, Series, Sponsoring } from '@/types/nostr'
import { extractTagsFromEvent } from './nostrTagSystem'
import { buildObjectId, parseObjectId } from './urlGenerator'
import { generateHashId } from './hashIdGenerator'
@ -229,7 +229,7 @@ async function buildArticle(event: Event, tags: ReturnType<typeof extractTagsFro
createdAt: event.created_at,
zapAmount: tags.zapAmount ?? 800,
paid: false,
thumbnailUrl: tags.bannerUrl ?? tags.pictureUrl ?? '', // Required field with default
thumbnailUrl: (typeof tags.bannerUrl === 'string' ? tags.bannerUrl : typeof tags.pictureUrl === 'string' ? tags.pictureUrl : ''), // Required field with default
...(tags.invoice ? { invoice: tags.invoice } : {}),
...(tags.paymentHash ? { paymentHash: tags.paymentHash } : {}),
...(category ? { category } : {}),

51
lib/syncStorage.ts Normal file
View File

@ -0,0 +1,51 @@
import { storageService } from './storage/indexedDB'
const LAST_SYNC_DATE_KEY = 'last_sync_date'
const SYNC_STORAGE_SECRET = 'sync_storage_secret'
/**
* Get the last synchronization date
* Returns MIN_EVENT_DATE if no date is stored
*/
export async function getLastSyncDate(): Promise<number> {
try {
const stored = await storageService.get<number>(LAST_SYNC_DATE_KEY, SYNC_STORAGE_SECRET)
if (stored !== null && typeof stored === 'number') {
return stored
}
} catch (error) {
console.error('Error getting last sync date:', error)
}
// Return MIN_EVENT_DATE if no date is stored
const { MIN_EVENT_DATE } = await import('./platformConfig')
return MIN_EVENT_DATE
}
/**
* Store the last synchronization date
*/
export async function setLastSyncDate(timestamp: number): Promise<void> {
try {
await storageService.set(LAST_SYNC_DATE_KEY, timestamp, SYNC_STORAGE_SECRET)
} catch (error) {
console.error('Error setting last sync date:', error)
}
}
/**
* Calculate the number of days between two timestamps
*/
export function calculateDaysBetween(startTimestamp: number, endTimestamp: number): number {
const startDate = new Date(startTimestamp * 1000)
const endDate = new Date(endTimestamp * 1000)
const diffTime = endDate.getTime() - startDate.getTime()
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
return Math.max(0, diffDays)
}
/**
* Get the current date as Unix timestamp
*/
export function getCurrentTimestamp(): number {
return Math.floor(Date.now() / 1000)
}

View File

@ -67,20 +67,23 @@ async function fetchAndCachePublications(
}
// Cache each publication
for (const [hash, hashEvents] of eventsByHashId.entries()) {
for (const [_hash, hashEvents] of eventsByHashId.entries()) {
const latestEvent = getLatestVersion(hashEvents)
if (latestEvent) {
const extracted = await extractPublicationFromEvent(latestEvent)
if (extracted?.hash) {
if (extracted) {
const parsed = parseObjectId(extracted.id)
const extractedHash = parsed.hash ?? extracted.id
const extractedIndex = parsed.index ?? 0
const tags = extractTagsFromEvent(latestEvent)
await objectCache.set(
'publication',
extracted.hash,
extractedHash,
latestEvent,
extracted,
tags.version ?? 0,
tags.hidden ?? false,
extracted.index
extractedIndex
)
}
}
@ -158,20 +161,23 @@ async function fetchAndCacheSeries(
}
// Cache each series
for (const [hash, hashEvents] of eventsByHashId.entries()) {
for (const [_hash, hashEvents] of eventsByHashId.entries()) {
const latestEvent = getLatestVersion(hashEvents)
if (latestEvent) {
const extracted = await extractSeriesFromEvent(latestEvent)
if (extracted?.hash) {
if (extracted) {
const parsed = parseObjectId(extracted.id)
const extractedHash = parsed.hash ?? extracted.id
const extractedIndex = parsed.index ?? 0
const tags = extractTagsFromEvent(latestEvent)
await objectCache.set(
'series',
extracted.hash,
extractedHash,
latestEvent,
extracted,
tags.version ?? 0,
tags.hidden ?? false,
extracted.index
extractedIndex
)
}
}
@ -233,9 +239,6 @@ async function fetchAndCachePurchases(
for (const event of events) {
const extracted = await extractPurchaseFromEvent(event)
if (extracted) {
const parsed = parseObjectId(extracted.id)
const hash = parsed.hash ?? extracted.id
const index = parsed.index ?? 0
// Parse to Purchase object for cache
const { parsePurchaseFromEvent } = await import('./nostrEventParsing')
const purchase = await parsePurchaseFromEvent(event)
@ -386,11 +389,22 @@ async function fetchAndCacheReviewTips(
})
}
export interface SyncProgress {
currentDay: number
totalDays: number
completed: boolean
}
/**
* Synchronize all user content to IndexedDB cache
* Fetches profile, series, publications, purchases, sponsoring, and review tips and caches them
* @param userPubkey - The user's public key
* @param onProgress - Optional callback to report progress (currentDay, totalDays, completed)
*/
export async function syncUserContentToCache(userPubkey: string): Promise<void> {
export async function syncUserContentToCache(
userPubkey: string,
onProgress?: (progress: SyncProgress) => void
): Promise<void> {
try {
const pool = nostrService.getPool()
if (!pool) {
@ -400,23 +414,63 @@ export async function syncUserContentToCache(userPubkey: string): Promise<void>
const poolWithSub = pool as unknown as SimplePoolWithSub
// Get last sync date and calculate days
const { getLastSyncDate, setLastSyncDate, getCurrentTimestamp, calculateDaysBetween } = await import('./syncStorage')
const lastSyncDate = await getLastSyncDate()
const currentTimestamp = getCurrentTimestamp()
const totalDays = calculateDaysBetween(lastSyncDate, currentTimestamp)
// Report initial progress
if (onProgress) {
onProgress({ currentDay: 0, totalDays, completed: false })
}
let currentDay = 0
// Fetch and cache author profile (already caches itself)
await fetchAuthorPresentationFromPool(poolWithSub, userPubkey)
currentDay++
if (onProgress) {
onProgress({ currentDay, totalDays, completed: false })
}
// Fetch and cache all series
await fetchAndCacheSeries(poolWithSub, userPubkey)
currentDay++
if (onProgress) {
onProgress({ currentDay, totalDays, completed: false })
}
// Fetch and cache all publications
await fetchAndCachePublications(poolWithSub, userPubkey)
currentDay++
if (onProgress) {
onProgress({ currentDay, totalDays, completed: false })
}
// Fetch and cache all purchases (as payer)
await fetchAndCachePurchases(poolWithSub, userPubkey)
currentDay++
if (onProgress) {
onProgress({ currentDay, totalDays, completed: false })
}
// Fetch and cache all sponsoring (as author)
await fetchAndCacheSponsoring(poolWithSub, userPubkey)
currentDay++
if (onProgress) {
onProgress({ currentDay, totalDays, completed: false })
}
// Fetch and cache all review tips (as author)
await fetchAndCacheReviewTips(poolWithSub, userPubkey)
currentDay++
if (onProgress) {
onProgress({ currentDay, totalDays, completed: true })
}
// Store the current timestamp as last sync date
await setLastSyncDate(currentTimestamp)
} catch (error) {
console.error('Error syncing user content to cache:', error)
// Don't throw - this is a background operation

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import type { ArticleFilters } from '@/components/ArticleFilters'
import type { NostrProfile } from '@/types/nostr'
import type { Article, NostrProfile } from '@/types/nostr'
import { ProfileView } from '@/components/ProfileView'
import { useNostrAuth } from '@/hooks/useNostrAuth'
import { useUserArticles } from '@/hooks/useUserArticles'

View File

@ -236,6 +236,11 @@ settings.keyManagement.recovery.copy=Copy Recovery Words
settings.keyManagement.recovery.copied=✓ Copied!
settings.keyManagement.recovery.newNpub=Your new public key (npub)
settings.keyManagement.recovery.done=Done
settings.sync.title=Notes Synchronization
settings.sync.start=Start Synchronization
settings.sync.daysRange=From {{startDate}} to {{endDate}} ({{days}} days)
settings.sync.progress=Day {{current}} of {{total}}
settings.sync.completed=Everything is synchronized
settings.nip95.title=NIP-95 Upload Endpoints
settings.nip95.loading=Loading...
settings.nip95.error.loadFailed=Failed to load NIP-95 APIs

View File

@ -236,6 +236,11 @@ settings.keyManagement.recovery.copy=Copier les mots de récupération
settings.keyManagement.recovery.copied=✓ Copié !
settings.keyManagement.recovery.newNpub=Votre nouvelle clé publique (npub)
settings.keyManagement.recovery.done=Terminé
settings.sync.title=Synchronisation des notes
settings.sync.start=Démarrer la synchronisation
settings.sync.daysRange=Du {{startDate}} au {{endDate}} ({{days}} jours)
settings.sync.progress=Jour {{current}} sur {{total}}
settings.sync.completed=Tout est synchronisé
settings.language.title=Langue de préférence
settings.language.description=Choisissez votre langue préférée pour l'interface
settings.language.loading=Chargement...