From 526fb5af6fbd25f74e8ffb2c0736563ca910901a Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Fri, 9 Jan 2026 09:22:30 +0100 Subject: [PATCH] lint fix wip --- components/AuthorPresentationEditor.tsx | 3 +- components/Nip95ConfigManager.tsx | 3 +- components/RelayManager.tsx | 3 +- components/SponsoringForm.tsx | 55 ++++++++-- hooks/useDocs.ts | 2 +- hooks/useI18n.ts | 4 +- lib/alby.ts | 20 ++-- lib/articleMutations.ts | 17 ++-- lib/articlePublisherHelpersPresentation.ts | 14 +-- lib/articlePublisherPublish.ts | 3 +- lib/hooks/useSyncProgress.ts | 38 +++---- lib/mnemonicIcons.ts | 10 +- lib/nostr.ts | 6 +- lib/notificationDetector.ts | 45 ++++----- lib/objectCache.ts | 16 +-- lib/platformCommissions.ts | 22 ++-- lib/platformSync.ts | 4 +- lib/userConfirm.ts | 112 ++++++++++++++++++++- lib/writeService.ts | 8 +- pages/api/nip95-upload.ts | 2 +- 20 files changed, 260 insertions(+), 127 deletions(-) diff --git a/components/AuthorPresentationEditor.tsx b/components/AuthorPresentationEditor.tsx index 3251244..867cb15 100644 --- a/components/AuthorPresentationEditor.tsx +++ b/components/AuthorPresentationEditor.tsx @@ -308,7 +308,8 @@ function useAuthorPresentationState(pubkey: string | null, existingAuthorName?: return } - if (!userConfirm(t('presentation.delete.confirm'))) { + const confirmed = await userConfirm(t('presentation.delete.confirm')) + if (!confirmed) { return } diff --git a/components/Nip95ConfigManager.tsx b/components/Nip95ConfigManager.tsx index f32c9cd..207d71b 100644 --- a/components/Nip95ConfigManager.tsx +++ b/components/Nip95ConfigManager.tsx @@ -179,7 +179,8 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps): } async function handleRemoveApi(id: string): Promise { - if (!userConfirm(t('settings.nip95.remove.confirm'))) { + const confirmed = await userConfirm(t('settings.nip95.remove.confirm')) + if (!confirmed) { return } diff --git a/components/RelayManager.tsx b/components/RelayManager.tsx index dcb9413..12de9d9 100644 --- a/components/RelayManager.tsx +++ b/components/RelayManager.tsx @@ -210,7 +210,8 @@ export function RelayManager({ onConfigChange }: RelayManagerProps): React.React } async function handleRemoveRelay(id: string): Promise { - if (!userConfirm(t('settings.relay.remove.confirm'))) { + const confirmed = await userConfirm(t('settings.relay.remove.confirm')) + if (!confirmed) { return } diff --git a/components/SponsoringForm.tsx b/components/SponsoringForm.tsx index 2cc9ce6..239e09f 100644 --- a/components/SponsoringForm.tsx +++ b/components/SponsoringForm.tsx @@ -16,6 +16,12 @@ export function SponsoringForm({ author, onSuccess, onCancel }: SponsoringFormPr const [text, setText] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const [instructions, setInstructions] = useState<{ + authorAddress: string + platformAddress: string + authorBtc: string + platformBtc: string + } | null>(null) const handleSubmit = async (e: React.FormEvent): Promise => { e.preventDefault() @@ -67,16 +73,15 @@ export function SponsoringForm({ author, onSuccess, onCancel }: SponsoringFormPr totalAmount: result.split.totalSats, }) - // Show instructions to user - alert(t('sponsoring.form.instructions', { + // Show instructions inline (no-alert) + setInstructions({ authorAddress: result.authorAddress, platformAddress: result.platformAddress, - authorAmount: (result.split.authorSats / 100_000_000).toFixed(8), - platformAmount: (result.split.platformSats / 100_000_000).toFixed(8), - })) + authorBtc: (result.split.authorSats / 100_000_000).toFixed(8), + platformBtc: (result.split.platformSats / 100_000_000).toFixed(8), + }) setText('') - onSuccess?.() } catch (submitError) { setError(submitError instanceof Error ? submitError.message : t('sponsoring.form.error.paymentFailed')) } finally { @@ -108,6 +113,44 @@ export function SponsoringForm({ author, onSuccess, onCancel }: SponsoringFormPr ) } + if (instructions) { + return ( +
+

{t('sponsoring.form.title')}

+

+ {t('sponsoring.form.instructions', { + authorAddress: instructions.authorAddress, + platformAddress: instructions.platformAddress, + authorAmount: instructions.authorBtc, + platformAmount: instructions.platformBtc, + })} +

+
+ + +
+
+ ) + } + return (
{ void handleSubmit(e) diff --git a/hooks/useDocs.ts b/hooks/useDocs.ts index 3f1ed3b..cd39d38 100644 --- a/hooks/useDocs.ts +++ b/hooks/useDocs.ts @@ -50,7 +50,7 @@ export function useDocs(docs: DocLink[]): { }, [docs]) useEffect(() => { - loadDoc('user-guide') + void loadDoc('user-guide') }, [loadDoc]) return { diff --git a/hooks/useI18n.ts b/hooks/useI18n.ts index 35d6a38..f7621c2 100644 --- a/hooks/useI18n.ts +++ b/hooks/useI18n.ts @@ -31,12 +31,12 @@ export function useI18n(locale: Locale = 'fr'): { if (frResponse.ok) { const frText = await frResponse.text() - await loadTranslations('fr', frText) + loadTranslations('fr', frText) } if (enResponse.ok) { const enText = await enResponse.text() - await loadTranslations('en', enText) + loadTranslations('en', enText) } setLocale(initialLocale) diff --git a/lib/alby.ts b/lib/alby.ts index d21161e..c886232 100644 --- a/lib/alby.ts +++ b/lib/alby.ts @@ -193,19 +193,23 @@ export class AlbyService { // Generate a simple hash-like identifier // In practice, payment verification should be done via zap receipts if (typeof window !== 'undefined' && window.crypto) { - // Use a simple hash for identification - let hash = 0 - for (let i = 0; i < invoice.length; i++) { - const char = invoice.charCodeAt(i) - hash = ((hash << 5) - hash) + char - hash = hash & hash // Convert to 32-bit integer - } - return Math.abs(hash).toString(16).padStart(16, '0') + const hash32 = hashStringToUint32(invoice) + return hash32.toString(16).padStart(16, '0') } return Date.now().toString(16) } } +function hashStringToUint32(value: string): number { + // Deterministic 32-bit hash without bitwise operators (rule: no-bitwise) + const mod = 2n ** 32n + let hash = 0n + for (let i = 0; i < value.length; i++) { + hash = (hash * 31n + BigInt(value.charCodeAt(i))) % mod + } + return Number(hash) +} + // Singleton instance let albyServiceInstance: AlbyService | null = null diff --git a/lib/articleMutations.ts b/lib/articleMutations.ts index 38ad529..7559d21 100644 --- a/lib/articleMutations.ts +++ b/lib/articleMutations.ts @@ -32,6 +32,7 @@ function requireCategory(category?: ArticleDraft['category']): asserts category } async function ensurePresentation(authorPubkey: string): Promise { + const { articlePublisher } = await import('./articlePublisher') const presentation = await articlePublisher.getAuthorPresentation(authorPubkey) if (!presentation) { throw new Error('Vous devez créer un article de présentation avant de publier des articles.') @@ -659,14 +660,14 @@ export async function deleteArticleEvent(articleId: string, authorPubkey: string if (!privateKey) { throw new Error('Private key required for signing') } - const { writeOrchestrator } = await import('./writeOrchestrator') - writeOrchestrator.setPrivateKey(privateKey) + const { writeOrchestrator: writeOrchestratorInstance } = await import('./writeOrchestrator') + writeOrchestratorInstance.setPrivateKey(privateKey) // Finalize event - const { finalizeEvent } = await import('nostr-tools') - const { hexToBytes } = await import('nostr-tools/utils') - const secretKey = hexToBytes(privateKey) - const event = finalizeEvent(deleteEventTemplate, secretKey) + const { finalizeEvent: finalizeNostrEvent } = await import('nostr-tools') + const { hexToBytes: hexToBytesUtil } = await import('nostr-tools/utils') + const secretKey = hexToBytesUtil(privateKey) + const event = finalizeNostrEvent(deleteEventTemplate, secretKey) // Get active relays const { relaySessionManager } = await import('./relaySessionManager') @@ -675,7 +676,7 @@ export async function deleteArticleEvent(articleId: string, authorPubkey: string const relays = activeRelays.length > 0 ? activeRelays : [await getPrimaryRelay()] // Publish via writeOrchestrator (parallel network + local write) - const result = await writeOrchestrator.writeAndPublish( + const result = await writeOrchestratorInstance.writeAndPublish( { objectType: 'publication', hash, @@ -694,5 +695,5 @@ export async function deleteArticleEvent(articleId: string, authorPubkey: string } // Re-export for convenience to avoid circular imports in hooks -import { articlePublisher } from './articlePublisher' +export { articlePublisher } from './articlePublisher' export const getStoredContent = getStoredPrivateContent diff --git a/lib/articlePublisherHelpersPresentation.ts b/lib/articlePublisherHelpersPresentation.ts index 6887a98..aa773ef 100644 --- a/lib/articlePublisherHelpersPresentation.ts +++ b/lib/articlePublisherHelpersPresentation.ts @@ -193,19 +193,9 @@ export async function parsePresentationEvent(event: Event): Promise { onCompleteRef.current = onComplete @@ -72,25 +91,6 @@ export function useSyncProgress(options: UseSyncProgressOptions = {}): UseSyncPr }, maxDuration) } - const stopMonitoring = (): void => { - if (!isMonitoringRef.current) { - return - } - - isMonitoringRef.current = false - setIsSyncing(false) - - if (intervalRef.current) { - clearInterval(intervalRef.current) - intervalRef.current = null - } - - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - timeoutRef.current = null - } - } - // Cleanup on unmount useEffect(() => { return () => { diff --git a/lib/mnemonicIcons.ts b/lib/mnemonicIcons.ts index 17d5b14..b8183ff 100644 --- a/lib/mnemonicIcons.ts +++ b/lib/mnemonicIcons.ts @@ -83,13 +83,13 @@ function expandDictionary(): MnemonicIcon[] { const DICTIONARY = expandDictionary() function hashString(str: string): number { - let hash = 0 + // Deterministic hash without bitwise operators (rule: no-bitwise) + const mod = 2n ** 32n + let hash = 0n for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i) - hash = ((hash << 5) - hash) + char - hash = hash & hash + hash = (hash * 31n + BigInt(str.charCodeAt(i))) % mod } - return Math.abs(hash) + return Number(hash) } export function generateMnemonicIcons(pubkey: string): string[] { diff --git a/lib/nostr.ts b/lib/nostr.ts index 5452810..64466b5 100644 --- a/lib/nostr.ts +++ b/lib/nostr.ts @@ -496,7 +496,7 @@ class NostrService { * Searches all object types to find and update the event */ private async updatePublishedStatus(eventId: string, published: false | string[]): Promise { - const { objectCache } = await import('./objectCache') + const { objectCache: objectCacheModule } = await import('./objectCache') const objectTypes: Array = ['author', 'series', 'publication', 'review', 'purchase', 'sponsoring', 'review_tip', 'payment_note'] // Load writeService once @@ -505,7 +505,7 @@ class NostrService { // First try to find in unpublished objects (faster) for (const objectType of objectTypes) { try { - const unpublished = await objectCache.getUnpublished(objectType) + const unpublished = await objectCacheModule.getUnpublished(objectType) const matching = unpublished.find((obj) => obj.event.id === eventId) if (matching) { await writeService.updatePublished(objectType, matching.id, published) @@ -521,7 +521,7 @@ class NostrService { for (const objectType of objectTypes) { try { // Use getAll to search all objects - const allObjects = await objectCache.getAll(objectType) + const allObjects = await objectCacheModule.getAll(objectType) const matching = allObjects.find((obj) => { const cachedObj = obj as CachedObject return cachedObj.event?.id === eventId diff --git a/lib/notificationDetector.ts b/lib/notificationDetector.ts index 1e88172..76e7e9a 100644 --- a/lib/notificationDetector.ts +++ b/lib/notificationDetector.ts @@ -162,33 +162,28 @@ class NotificationDetector { }) for (const obj of userObjects) { - if (!Array.isArray(obj.published) || obj.published.length === 0) { - continue - } + if (Array.isArray(obj.published) && obj.published.length > 0) { + const eventId = obj.id.split(':')[1] ?? obj.id + const existing = await notificationService.getNotificationByEventId(eventId) + const alreadyNotified = existing?.type === 'published' + const recentlyCreated = obj.createdAt * 1000 > oneHourAgo - const eventId = obj.id.split(':')[1] ?? obj.id - const existing = await notificationService.getNotificationByEventId(eventId) - if (existing?.type === 'published') { - continue + if (!alreadyNotified && recentlyCreated) { + const relays = obj.published + await notificationService.createNotification({ + type: 'published', + objectType, + objectId: obj.id, + eventId, + data: { + relays, + object: obj, + title: 'Publication réussie', + message: `Votre contenu a été publié sur ${relays.length} relais`, + }, + }) + } } - - if (obj.createdAt * 1000 <= oneHourAgo) { - continue - } - - const relays = obj.published - await notificationService.createNotification({ - type: 'published', - objectType, - objectId: obj.id, - eventId, - data: { - relays, - object: obj, - title: 'Publication réussie', - message: `Votre contenu a été publié sur ${relays.length} relais`, - }, - }) } } catch (error) { console.error(`[NotificationDetector] Error scanning published status for ${objectType}:`, error) diff --git a/lib/objectCache.ts b/lib/objectCache.ts index 6bb769b..5d10cf3 100644 --- a/lib/objectCache.ts +++ b/lib/objectCache.ts @@ -260,12 +260,12 @@ class ObjectCacheService { cursor.continue() } else { // Sort by version descending and return the latest - if (objects.length > 0) { - objects.sort((a, b) => b.version - a.version) - resolve(objects[0]?.parsed ?? null) - } else { + if (objects.length === 0) { resolve(null) + return } + objects.sort((a, b) => b.version - a.version) + resolve(objects[0]?.parsed ?? null) } } @@ -336,12 +336,12 @@ class ObjectCacheService { cursor.continue() } else { // Sort by version descending and return the latest - if (objects.length > 0) { - objects.sort((a, b) => b.version - a.version) - resolve((objects[0]?.parsed ?? null) as AuthorPresentationArticle | null) - } else { + if (objects.length === 0) { resolve(null) + return } + objects.sort((a, b) => b.version - a.version) + resolve((objects[0]?.parsed ?? null) as AuthorPresentationArticle | null) } } diff --git a/lib/platformCommissions.ts b/lib/platformCommissions.ts index 5ec3803..19e52ef 100644 --- a/lib/platformCommissions.ts +++ b/lib/platformCommissions.ts @@ -129,24 +129,20 @@ export function verifyPaymentSplit( platformAmount?: number ): boolean { switch (type) { - case 'article': + case 'article': { const articleSplit = calculateArticleSplit(totalAmount) - return ( - articleSplit.author === (authorAmount ?? 0) && articleSplit.platform === (platformAmount ?? 0) - ) + return articleSplit.author === (authorAmount ?? 0) && articleSplit.platform === (platformAmount ?? 0) + } - case 'review': + case 'review': { const reviewSplit = calculateReviewSplit(totalAmount) - return ( - reviewSplit.reviewer === (authorAmount ?? 0) && reviewSplit.platform === (platformAmount ?? 0) - ) + return reviewSplit.reviewer === (authorAmount ?? 0) && reviewSplit.platform === (platformAmount ?? 0) + } - case 'sponsoring': + case 'sponsoring': { const sponsoringSplit = calculateSponsoringSplit(totalAmount) - return ( - sponsoringSplit.authorSats === (authorAmount ?? 0) && - sponsoringSplit.platformSats === (platformAmount ?? 0) - ) + return sponsoringSplit.authorSats === (authorAmount ?? 0) && sponsoringSplit.platformSats === (platformAmount ?? 0) + } default: return false diff --git a/lib/platformSync.ts b/lib/platformSync.ts index 811e2d6..efff697 100644 --- a/lib/platformSync.ts +++ b/lib/platformSync.ts @@ -188,7 +188,7 @@ class PlatformSyncService { sub.on('eose', (): void => { console.warn(`[PlatformSync] Relay ${relayUrl} sent EOSE signal`) - finalize() + void finalize() resolve() }) @@ -424,7 +424,7 @@ class PlatformSyncService { if (isReady) { await swClient.stopPlatformSync() } - } catch (error) { + } catch { // Ignore errors } } diff --git a/lib/userConfirm.ts b/lib/userConfirm.ts index 69ed7f5..7f2bb38 100644 --- a/lib/userConfirm.ts +++ b/lib/userConfirm.ts @@ -8,9 +8,111 @@ * that cannot be replicated with React modals without significant refactoring. * Used only for critical destructive actions (delete operations). */ -export function userConfirm(message: string): boolean { - // window.confirm is the native browser confirmation dialog - // This is intentionally used here for critical confirmations - // that must block the UI thread until user responds - return window.confirm(message) +export function userConfirm(message: string): Promise { + return confirmOverlay(message) +} + +function confirmOverlay(message: string): Promise { + const doc = globalThis.document + if (!doc) { + return Promise.resolve(false) + } + + return new Promise((resolve) => { + const overlay = doc.createElement('div') + overlay.setAttribute('role', 'dialog') + overlay.setAttribute('aria-modal', 'true') + overlay.tabIndex = -1 + overlay.style.position = 'fixed' + overlay.style.inset = '0' + overlay.style.background = 'rgba(0,0,0,0.6)' + overlay.style.display = 'flex' + overlay.style.alignItems = 'center' + overlay.style.justifyContent = 'center' + overlay.style.zIndex = '9999' + + const panel = doc.createElement('div') + panel.style.background = '#fff' + panel.style.borderRadius = '12px' + panel.style.padding = '16px' + panel.style.maxWidth = '520px' + panel.style.width = 'calc(100% - 32px)' + panel.style.boxShadow = '0 10px 30px rgba(0,0,0,0.35)' + + const text = doc.createElement('p') + text.textContent = message + text.style.margin = '0 0 16px 0' + text.style.color = '#111827' + + const buttons = doc.createElement('div') + buttons.style.display = 'flex' + buttons.style.gap = '12px' + buttons.style.justifyContent = 'flex-end' + + const cancel = doc.createElement('button') + cancel.type = 'button' + cancel.textContent = 'Cancel' + cancel.style.padding = '8px 12px' + cancel.style.borderRadius = '10px' + cancel.style.border = '1px solid #e5e7eb' + cancel.style.background = '#f3f4f6' + + const confirm = doc.createElement('button') + confirm.type = 'button' + confirm.textContent = 'Confirm' + confirm.style.padding = '8px 12px' + confirm.style.borderRadius = '10px' + confirm.style.border = '1px solid #ef4444' + confirm.style.background = '#fee2e2' + confirm.style.color = '#991b1b' + + buttons.append(cancel, confirm) + panel.append(text, buttons) + overlay.append(panel) + doc.body.append(overlay) + + overlay.focus() + + let resolved = false + + const resolveOnce = (next: boolean): void => { + if (resolved) { + return + } + resolved = true + cleanup() + resolve(next) + } + + function onCancel(): void { + resolveOnce(false) + } + + function onConfirm(): void { + resolveOnce(true) + } + + function onKeyDown(e: KeyboardEvent): void { + if (e.key === 'Escape') { + e.preventDefault() + resolveOnce(false) + return + } + if (e.key === 'Enter') { + e.preventDefault() + resolveOnce(true) + } + } + + function cleanup(): void { + overlay.removeEventListener('keydown', onKeyDown) + cancel.removeEventListener('click', onCancel) + confirm.removeEventListener('click', onConfirm) + overlay.remove() + } + + cancel.addEventListener('click', onCancel) + confirm.addEventListener('click', onConfirm) + overlay.addEventListener('keydown', onKeyDown) + }) } diff --git a/lib/writeService.ts b/lib/writeService.ts index b83f9d2..460caec 100644 --- a/lib/writeService.ts +++ b/lib/writeService.ts @@ -262,9 +262,9 @@ class WriteService { resolve() } else if (responseType === 'ERROR') { const errorData = readWorkerErrorData(responseData) - const {taskId} = errorData + const { taskId } = errorData const isUpdatePublished = - errorData.originalType === 'UPDATE_PUBLISHED' || (taskId !== undefined && taskId.startsWith('UPDATE_PUBLISHED')) + errorData.originalType === 'UPDATE_PUBLISHED' || taskId?.startsWith('UPDATE_PUBLISHED') === true if (!isUpdatePublished) { return } @@ -320,9 +320,9 @@ class WriteService { resolve() } else if (responseType === 'ERROR') { const errorData = readWorkerErrorData(responseData) - const {taskId} = errorData + const { taskId } = errorData const isCreateNotification = - errorData.originalType === 'CREATE_NOTIFICATION' || (taskId !== undefined && taskId.startsWith('CREATE_NOTIFICATION')) + errorData.originalType === 'CREATE_NOTIFICATION' || taskId?.startsWith('CREATE_NOTIFICATION') === true if (!isCreateNotification) { return } diff --git a/pages/api/nip95-upload.ts b/pages/api/nip95-upload.ts index ee9f789..47991c6 100644 --- a/pages/api/nip95-upload.ts +++ b/pages/api/nip95-upload.ts @@ -361,7 +361,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // Try to extract error message from HTML if possible const titleMatch = response.body.match(/]*>([^<]+)<\/title>/i) const h1Match = response.body.match(/]*>([^<]+)<\/h1>/i) - const errorText = titleMatch?.[1] || h1Match?.[1] || 'HTML error page returned' + const errorText = titleMatch?.[1] ?? h1Match?.[1] ?? 'HTML error page returned' // Check if it's a 404 or other error page const is404 = response.body.includes('404') || response.body.includes('Not Found') || titleMatch?.[1]?.includes('404')