lint fix wip
This commit is contained in:
parent
390f895920
commit
303c0bf7df
@ -202,7 +202,7 @@ const ArticleFieldsRight = ({
|
|||||||
<MarkdownEditorTwoColumns
|
<MarkdownEditorTwoColumns
|
||||||
value={draft.content}
|
value={draft.content}
|
||||||
onChange={(value) => onDraftChange({ ...draft, content: value })}
|
onChange={(value) => onDraftChange({ ...draft, content: value })}
|
||||||
pages={draft.pages}
|
{...(draft.pages ? { pages: draft.pages } : {})}
|
||||||
onPagesChange={(pages) => onDraftChange({ ...draft, pages })}
|
onPagesChange={(pages) => onDraftChange({ ...draft, pages })}
|
||||||
onMediaAdd={(media: MediaRef) => {
|
onMediaAdd={(media: MediaRef) => {
|
||||||
const nextMedia = [...(draft.media ?? []), media]
|
const nextMedia = [...(draft.media ?? []), media]
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { nostrAuthService } from '@/lib/nostrAuth'
|
|||||||
import { keyManagementService } from '@/lib/keyManagement'
|
import { keyManagementService } from '@/lib/keyManagement'
|
||||||
import { nip19 } from 'nostr-tools'
|
import { nip19 } from 'nostr-tools'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
import { SyncProgressBar } from './SyncProgressBar'
|
||||||
|
|
||||||
interface PublicKeys {
|
interface PublicKeys {
|
||||||
publicKey: string
|
publicKey: string
|
||||||
@ -251,6 +252,9 @@ export function KeyManagementManager(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Sync Progress Bar */}
|
||||||
|
{publicKeys && <SyncProgressBar />}
|
||||||
|
|
||||||
{!publicKeys && !accountExists && (
|
{!publicKeys && !accountExists && (
|
||||||
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4 mb-6">
|
<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>
|
<p className="text-yellow-400 font-semibold mb-2">{t('settings.keyManagement.noAccount.title')}</p>
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export function MarkdownEditorTwoColumns({
|
|||||||
}}
|
}}
|
||||||
uploading={uploading}
|
uploading={uploading}
|
||||||
error={error}
|
error={error}
|
||||||
onAddPage={onPagesChange ? handleAddPage : undefined}
|
{...(onPagesChange ? { onAddPage: handleAddPage } : {})}
|
||||||
/>
|
/>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -205,8 +205,8 @@ function PagesManager({
|
|||||||
onContentChange={(content) => onPageContentChange(page.number, content)}
|
onContentChange={(content) => onPageContentChange(page.number, content)}
|
||||||
onTypeChange={(type) => onPageTypeChange(page.number, type)}
|
onTypeChange={(type) => onPageTypeChange(page.number, type)}
|
||||||
onRemove={() => onRemovePage(page.number)}
|
onRemove={() => onRemovePage(page.number)}
|
||||||
onImageUpload={(file) => {
|
onImageUpload={async (file) => {
|
||||||
void onImageUpload(file, page.number)
|
await onImageUpload(file, page.number)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -41,18 +41,18 @@ export function ReviewForm({ article, onSuccess, onCancel }: ReviewFormProps): R
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const category = article.category ?? 'science-fiction'
|
const category = article.category === 'author-presentation' ? 'science-fiction' : (article.category ?? 'science-fiction')
|
||||||
const seriesId = article.seriesId ?? ''
|
const seriesId = article.seriesId ?? ''
|
||||||
|
|
||||||
await publishReview({
|
await publishReview({
|
||||||
articleId: article.id,
|
articleId: article.id,
|
||||||
seriesId,
|
seriesId,
|
||||||
category,
|
category: category === 'science-fiction' || category === 'scientific-research' ? category : 'science-fiction',
|
||||||
authorPubkey: article.pubkey,
|
authorPubkey: article.pubkey,
|
||||||
reviewerPubkey: pubkey,
|
reviewerPubkey: pubkey,
|
||||||
content: content.trim(),
|
content: content.trim(),
|
||||||
title: title.trim() || undefined,
|
...(title.trim() ? { title: title.trim() } : {}),
|
||||||
text: text.trim() || undefined,
|
...(text.trim() ? { text: text.trim() } : {}),
|
||||||
authorPrivateKey: privateKey,
|
authorPrivateKey: privateKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -38,17 +38,17 @@ export function ReviewTipForm({ review, article, onSuccess, onCancel }: ReviewTi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const split = calculateReviewSplit()
|
const split = calculateReviewSplit()
|
||||||
const category = article.category === 'science-fiction' ? 'sciencefiction' : article.category === 'scientific-research' ? 'research' : 'sciencefiction'
|
|
||||||
|
|
||||||
// Build zap request tags
|
// 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({
|
const zapRequestTags = buildReviewTipZapRequestTags({
|
||||||
articleId: article.id,
|
articleId: article.id,
|
||||||
reviewId: review.id,
|
reviewId: review.id,
|
||||||
authorPubkey: article.pubkey,
|
authorPubkey: article.pubkey,
|
||||||
reviewerPubkey: review.reviewerPubkey,
|
reviewerPubkey: review.reviewerPubkey,
|
||||||
category: article.category,
|
...(category ? { category } : {}),
|
||||||
seriesId: article.seriesId,
|
...(article.seriesId ? { seriesId: article.seriesId } : {}),
|
||||||
text: text.trim() || undefined,
|
...(text.trim() ? { text: text.trim() } : {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create zap request event (kind 9734) and publish it
|
// Create zap request event (kind 9734) and publish it
|
||||||
|
|||||||
160
components/SyncProgressBar.tsx
Normal file
160
components/SyncProgressBar.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState, type Dispatch, type SetStateAction } from 'react'
|
import { useEffect, useState, type Dispatch, type SetStateAction } from 'react'
|
||||||
import type { Article } from '@/types/nostr'
|
import type { Article } from '@/types/nostr'
|
||||||
|
import type { ArticleDraft } from '@/lib/articlePublisherTypes'
|
||||||
import { useArticleEditing } from '@/hooks/useArticleEditing'
|
import { useArticleEditing } from '@/hooks/useArticleEditing'
|
||||||
import { UserArticlesView } from './UserArticlesList'
|
import { UserArticlesView } from './UserArticlesList'
|
||||||
import { EditPanel } from './UserArticlesEditPanel'
|
import { EditPanel } from './UserArticlesEditPanel'
|
||||||
@ -135,16 +136,29 @@ function buildUpdatedArticle(
|
|||||||
pubkey: string,
|
pubkey: string,
|
||||||
newId: string
|
newId: string
|
||||||
): Article {
|
): 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 {
|
return {
|
||||||
id: newId,
|
id: newId,
|
||||||
|
hash,
|
||||||
|
version,
|
||||||
|
index,
|
||||||
pubkey,
|
pubkey,
|
||||||
title: draft.title,
|
title: draft.title,
|
||||||
preview: draft.preview,
|
preview: draft.preview,
|
||||||
content: '',
|
content: '',
|
||||||
|
description: draft.preview,
|
||||||
|
contentDescription: draft.preview,
|
||||||
createdAt: Math.floor(Date.now() / 1000),
|
createdAt: Math.floor(Date.now() / 1000),
|
||||||
zapAmount: draft.zapAmount,
|
zapAmount: draft.zapAmount,
|
||||||
paid: false,
|
paid: false,
|
||||||
|
thumbnailUrl: draft.bannerUrl ?? '',
|
||||||
...(draft.category ? { category: draft.category } : {}),
|
...(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 } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { storePrivateContent, getStoredPrivateContent } from './articleStorage'
|
|||||||
import { buildTags } from './nostrTagSystem'
|
import { buildTags } from './nostrTagSystem'
|
||||||
import { PLATFORM_SERVICE } from './platformConfig'
|
import { PLATFORM_SERVICE } from './platformConfig'
|
||||||
import { generateSeriesHashId, generatePublicationHashId } from './hashIdGenerator'
|
import { generateSeriesHashId, generatePublicationHashId } from './hashIdGenerator'
|
||||||
|
import { parseObjectId } from './urlGenerator'
|
||||||
import type { ArticleDraft, PublishedArticle } from './articlePublisher'
|
import type { ArticleDraft, PublishedArticle } from './articlePublisher'
|
||||||
import type { AlbyInvoice } from '@/types/alby'
|
import type { AlbyInvoice } from '@/types/alby'
|
||||||
import type { Review, Series } from '@/types/nostr'
|
import type { Review, Series } from '@/types/nostr'
|
||||||
@ -64,12 +65,20 @@ export async function publishSeries(params: {
|
|||||||
if (!published) {
|
if (!published) {
|
||||||
throw new Error('Failed to publish series')
|
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 {
|
return {
|
||||||
id: published.id,
|
id: published.id,
|
||||||
|
hash,
|
||||||
|
version,
|
||||||
|
index,
|
||||||
pubkey: params.authorPubkey,
|
pubkey: params.authorPubkey,
|
||||||
title: params.title,
|
title: params.title,
|
||||||
description: params.description,
|
description: params.description,
|
||||||
preview: params.preview ?? params.description.substring(0, 200),
|
preview: params.preview ?? params.description.substring(0, 200),
|
||||||
|
thumbnailUrl: params.coverUrl ?? '',
|
||||||
category,
|
category,
|
||||||
...(params.coverUrl ? { coverUrl: params.coverUrl } : {}),
|
...(params.coverUrl ? { coverUrl: params.coverUrl } : {}),
|
||||||
kindType: 'series',
|
kindType: 'series',
|
||||||
@ -156,12 +165,20 @@ export async function publishReview(params: {
|
|||||||
if (!published) {
|
if (!published) {
|
||||||
throw new Error('Failed to publish review')
|
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 {
|
return {
|
||||||
id: published.id,
|
id: published.id,
|
||||||
|
hash,
|
||||||
|
version,
|
||||||
|
index,
|
||||||
articleId: params.articleId,
|
articleId: params.articleId,
|
||||||
authorPubkey: params.authorPubkey,
|
authorPubkey: params.authorPubkey,
|
||||||
reviewerPubkey: params.reviewerPubkey,
|
reviewerPubkey: params.reviewerPubkey,
|
||||||
content: params.content,
|
content: params.content,
|
||||||
|
description: params.content.substring(0, 200),
|
||||||
createdAt: published.created_at,
|
createdAt: published.created_at,
|
||||||
...(params.title ? { title: params.title } : {}),
|
...(params.title ? { title: params.title } : {}),
|
||||||
...(params.text ? { text: params.text } : {}),
|
...(params.text ? { text: params.text } : {}),
|
||||||
|
|||||||
@ -181,8 +181,8 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
|
|||||||
authorName: profileData?.authorName ?? '',
|
authorName: profileData?.authorName ?? '',
|
||||||
presentation: profileData?.presentation ?? '',
|
presentation: profileData?.presentation ?? '',
|
||||||
contentDescription: profileData?.contentDescription ?? '',
|
contentDescription: profileData?.contentDescription ?? '',
|
||||||
mainnetAddress: profileData?.mainnetAddress ?? tags.mainnetAddress,
|
mainnetAddress: profileData?.mainnetAddress ?? (typeof tags.mainnetAddress === 'string' ? tags.mainnetAddress : undefined),
|
||||||
pictureUrl: profileData?.pictureUrl ?? tags.pictureUrl,
|
pictureUrl: profileData?.pictureUrl ?? (typeof tags.pictureUrl === 'string' ? tags.pictureUrl : undefined),
|
||||||
category: profileData?.category ?? tags.category ?? 'sciencefiction',
|
category: profileData?.category ?? tags.category ?? 'sciencefiction',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -202,7 +202,7 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
|
|||||||
content: event.content,
|
content: event.content,
|
||||||
description: profileData?.presentation ?? tags.description ?? '', // Required field
|
description: profileData?.presentation ?? tags.description ?? '', // Required field
|
||||||
contentDescription: profileData?.contentDescription ?? 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,
|
createdAt: event.created_at,
|
||||||
zapAmount: 0,
|
zapAmount: 0,
|
||||||
paid: true,
|
paid: true,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { Event } from 'nostr-tools'
|
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 { extractTagsFromEvent } from './nostrTagSystem'
|
||||||
import { buildObjectId, parseObjectId } from './urlGenerator'
|
import { buildObjectId, parseObjectId } from './urlGenerator'
|
||||||
import { generateHashId } from './hashIdGenerator'
|
import { generateHashId } from './hashIdGenerator'
|
||||||
@ -229,7 +229,7 @@ async function buildArticle(event: Event, tags: ReturnType<typeof extractTagsFro
|
|||||||
createdAt: event.created_at,
|
createdAt: event.created_at,
|
||||||
zapAmount: tags.zapAmount ?? 800,
|
zapAmount: tags.zapAmount ?? 800,
|
||||||
paid: false,
|
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.invoice ? { invoice: tags.invoice } : {}),
|
||||||
...(tags.paymentHash ? { paymentHash: tags.paymentHash } : {}),
|
...(tags.paymentHash ? { paymentHash: tags.paymentHash } : {}),
|
||||||
...(category ? { category } : {}),
|
...(category ? { category } : {}),
|
||||||
|
|||||||
51
lib/syncStorage.ts
Normal file
51
lib/syncStorage.ts
Normal 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)
|
||||||
|
}
|
||||||
@ -67,20 +67,23 @@ async function fetchAndCachePublications(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache each publication
|
// Cache each publication
|
||||||
for (const [hash, hashEvents] of eventsByHashId.entries()) {
|
for (const [_hash, hashEvents] of eventsByHashId.entries()) {
|
||||||
const latestEvent = getLatestVersion(hashEvents)
|
const latestEvent = getLatestVersion(hashEvents)
|
||||||
if (latestEvent) {
|
if (latestEvent) {
|
||||||
const extracted = await extractPublicationFromEvent(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)
|
const tags = extractTagsFromEvent(latestEvent)
|
||||||
await objectCache.set(
|
await objectCache.set(
|
||||||
'publication',
|
'publication',
|
||||||
extracted.hash,
|
extractedHash,
|
||||||
latestEvent,
|
latestEvent,
|
||||||
extracted,
|
extracted,
|
||||||
tags.version ?? 0,
|
tags.version ?? 0,
|
||||||
tags.hidden ?? false,
|
tags.hidden ?? false,
|
||||||
extracted.index
|
extractedIndex
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,20 +161,23 @@ async function fetchAndCacheSeries(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache each series
|
// Cache each series
|
||||||
for (const [hash, hashEvents] of eventsByHashId.entries()) {
|
for (const [_hash, hashEvents] of eventsByHashId.entries()) {
|
||||||
const latestEvent = getLatestVersion(hashEvents)
|
const latestEvent = getLatestVersion(hashEvents)
|
||||||
if (latestEvent) {
|
if (latestEvent) {
|
||||||
const extracted = await extractSeriesFromEvent(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)
|
const tags = extractTagsFromEvent(latestEvent)
|
||||||
await objectCache.set(
|
await objectCache.set(
|
||||||
'series',
|
'series',
|
||||||
extracted.hash,
|
extractedHash,
|
||||||
latestEvent,
|
latestEvent,
|
||||||
extracted,
|
extracted,
|
||||||
tags.version ?? 0,
|
tags.version ?? 0,
|
||||||
tags.hidden ?? false,
|
tags.hidden ?? false,
|
||||||
extracted.index
|
extractedIndex
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -233,9 +239,6 @@ async function fetchAndCachePurchases(
|
|||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const extracted = await extractPurchaseFromEvent(event)
|
const extracted = await extractPurchaseFromEvent(event)
|
||||||
if (extracted) {
|
if (extracted) {
|
||||||
const parsed = parseObjectId(extracted.id)
|
|
||||||
const hash = parsed.hash ?? extracted.id
|
|
||||||
const index = parsed.index ?? 0
|
|
||||||
// Parse to Purchase object for cache
|
// Parse to Purchase object for cache
|
||||||
const { parsePurchaseFromEvent } = await import('./nostrEventParsing')
|
const { parsePurchaseFromEvent } = await import('./nostrEventParsing')
|
||||||
const purchase = await parsePurchaseFromEvent(event)
|
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
|
* Synchronize all user content to IndexedDB cache
|
||||||
* Fetches profile, series, publications, purchases, sponsoring, and review tips and caches them
|
* 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 {
|
try {
|
||||||
const pool = nostrService.getPool()
|
const pool = nostrService.getPool()
|
||||||
if (!pool) {
|
if (!pool) {
|
||||||
@ -400,23 +414,63 @@ export async function syncUserContentToCache(userPubkey: string): Promise<void>
|
|||||||
|
|
||||||
const poolWithSub = pool as unknown as SimplePoolWithSub
|
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)
|
// Fetch and cache author profile (already caches itself)
|
||||||
await fetchAuthorPresentationFromPool(poolWithSub, userPubkey)
|
await fetchAuthorPresentationFromPool(poolWithSub, userPubkey)
|
||||||
|
currentDay++
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({ currentDay, totalDays, completed: false })
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch and cache all series
|
// Fetch and cache all series
|
||||||
await fetchAndCacheSeries(poolWithSub, userPubkey)
|
await fetchAndCacheSeries(poolWithSub, userPubkey)
|
||||||
|
currentDay++
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({ currentDay, totalDays, completed: false })
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch and cache all publications
|
// Fetch and cache all publications
|
||||||
await fetchAndCachePublications(poolWithSub, userPubkey)
|
await fetchAndCachePublications(poolWithSub, userPubkey)
|
||||||
|
currentDay++
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({ currentDay, totalDays, completed: false })
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch and cache all purchases (as payer)
|
// Fetch and cache all purchases (as payer)
|
||||||
await fetchAndCachePurchases(poolWithSub, userPubkey)
|
await fetchAndCachePurchases(poolWithSub, userPubkey)
|
||||||
|
currentDay++
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({ currentDay, totalDays, completed: false })
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch and cache all sponsoring (as author)
|
// Fetch and cache all sponsoring (as author)
|
||||||
await fetchAndCacheSponsoring(poolWithSub, userPubkey)
|
await fetchAndCacheSponsoring(poolWithSub, userPubkey)
|
||||||
|
currentDay++
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({ currentDay, totalDays, completed: false })
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch and cache all review tips (as author)
|
// Fetch and cache all review tips (as author)
|
||||||
await fetchAndCacheReviewTips(poolWithSub, userPubkey)
|
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) {
|
} catch (error) {
|
||||||
console.error('Error syncing user content to cache:', error)
|
console.error('Error syncing user content to cache:', error)
|
||||||
// Don't throw - this is a background operation
|
// Don't throw - this is a background operation
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import type { ArticleFilters } from '@/components/ArticleFilters'
|
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 { ProfileView } from '@/components/ProfileView'
|
||||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||||
import { useUserArticles } from '@/hooks/useUserArticles'
|
import { useUserArticles } from '@/hooks/useUserArticles'
|
||||||
|
|||||||
@ -236,6 +236,11 @@ settings.keyManagement.recovery.copy=Copy Recovery Words
|
|||||||
settings.keyManagement.recovery.copied=✓ Copied!
|
settings.keyManagement.recovery.copied=✓ Copied!
|
||||||
settings.keyManagement.recovery.newNpub=Your new public key (npub)
|
settings.keyManagement.recovery.newNpub=Your new public key (npub)
|
||||||
settings.keyManagement.recovery.done=Done
|
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.title=NIP-95 Upload Endpoints
|
||||||
settings.nip95.loading=Loading...
|
settings.nip95.loading=Loading...
|
||||||
settings.nip95.error.loadFailed=Failed to load NIP-95 APIs
|
settings.nip95.error.loadFailed=Failed to load NIP-95 APIs
|
||||||
|
|||||||
@ -236,6 +236,11 @@ settings.keyManagement.recovery.copy=Copier les mots de récupération
|
|||||||
settings.keyManagement.recovery.copied=✓ Copié !
|
settings.keyManagement.recovery.copied=✓ Copié !
|
||||||
settings.keyManagement.recovery.newNpub=Votre nouvelle clé publique (npub)
|
settings.keyManagement.recovery.newNpub=Votre nouvelle clé publique (npub)
|
||||||
settings.keyManagement.recovery.done=Terminé
|
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.title=Langue de préférence
|
||||||
settings.language.description=Choisissez votre langue préférée pour l'interface
|
settings.language.description=Choisissez votre langue préférée pour l'interface
|
||||||
settings.language.loading=Chargement...
|
settings.language.loading=Chargement...
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user