From 7c0dc68304fe754a912071f763ebc65ae1f9dabc Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Fri, 9 Jan 2026 08:45:54 +0100 Subject: [PATCH] lint fix wip --- components/LanguageSelector.tsx | 8 ++- components/LanguageSettingsManager.tsx | 8 ++- components/Nip95ConfigManager.tsx | 18 +++-- components/UnlockAccountModal.tsx | 14 +++- hooks/useNotifications.ts | 52 ++++++++------ lib/articlePublisherHelpersPresentation.ts | 64 ++++++++++++++--- lib/nostr.ts | 10 ++- lib/notificationDetector.ts | 55 +++++++------- lib/paymentPollingMain.ts | 83 ++++++++++++++-------- pages/api/nip95-upload.ts | 73 ++++++++++++++++--- pages/author/[pubkey].tsx | 5 +- 11 files changed, 268 insertions(+), 122 deletions(-) diff --git a/components/LanguageSelector.tsx b/components/LanguageSelector.tsx index 339545d..31caaca 100644 --- a/components/LanguageSelector.tsx +++ b/components/LanguageSelector.tsx @@ -60,10 +60,14 @@ export function LanguageSelector(): React.ReactElement { window.location.reload() } + const onLocaleClick = (locale: Locale): void => { + void handleLocaleChange(locale) + } + return (
- - + +
) } diff --git a/components/LanguageSettingsManager.tsx b/components/LanguageSettingsManager.tsx index a42fba7..bdf4571 100644 --- a/components/LanguageSettingsManager.tsx +++ b/components/LanguageSettingsManager.tsx @@ -63,6 +63,10 @@ export function LanguageSettingsManager(): React.ReactElement { window.location.reload() } + const onLocaleClick = (locale: Locale): void => { + void handleLocaleChange(locale) + } + if (loading) { return (
@@ -76,8 +80,8 @@ export function LanguageSettingsManager(): React.ReactElement {

{t('settings.language.title')}

{t('settings.language.description')}

- - + +
) diff --git a/components/Nip95ConfigManager.tsx b/components/Nip95ConfigManager.tsx index 500bdd0..f32c9cd 100644 --- a/components/Nip95ConfigManager.tsx +++ b/components/Nip95ConfigManager.tsx @@ -277,13 +277,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps): onDrop={(e) => { handleDrop(e, api.id) }} - className={`bg-cyber-dark border rounded p-4 space-y-3 transition-all ${ - draggedId === api.id - ? 'opacity-50 border-neon-cyan' - : dragOverId === api.id - ? 'border-neon-green shadow-lg' - : 'border-neon-cyan/30' - }`} + className={`bg-cyber-dark border rounded p-4 space-y-3 transition-all ${getApiCardClassName(api.id, draggedId, dragOverId)}`} >
@@ -377,3 +371,13 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps):
) } + +function getApiCardClassName(apiId: string, draggedId: string | null, dragOverId: string | null): string { + if (draggedId === apiId) { + return 'opacity-50 border-neon-cyan' + } + if (dragOverId === apiId) { + return 'border-neon-green shadow-lg' + } + return 'border-neon-cyan/30' +} diff --git a/components/UnlockAccountModal.tsx b/components/UnlockAccountModal.tsx index a7998e4..42aebc7 100644 --- a/components/UnlockAccountModal.tsx +++ b/components/UnlockAccountModal.tsx @@ -143,7 +143,7 @@ function WordInputs({ function useUnlockAccount(words: string[], setWords: (words: string[]) => void, setError: (error: string | null) => void): { handleWordChange: (index: number, value: string) => void - handlePaste: () => Promise + handlePaste: () => void } { const handleWordChange = (index: number, value: string): void => { const newWords = [...words] @@ -152,7 +152,7 @@ function useUnlockAccount(words: string[], setWords: (words: string[]) => void, setError(null) } - const handlePaste = async (): Promise => { + const handlePasteAsync = async (): Promise => { try { const text = await navigator.clipboard.readText() const pastedWords = text.trim().split(/\s+/).slice(0, 4) @@ -165,6 +165,10 @@ function useUnlockAccount(words: string[], setWords: (words: string[]) => void, } } + const handlePaste = (): void => { + void handlePasteAsync() + } + return { handleWordChange, handlePaste } } @@ -251,6 +255,10 @@ export function UnlockAccountModal({ onSuccess, onClose }: UnlockAccountModalPro } } + const onUnlock = (): void => { + void handleUnlock() + } + return (
@@ -261,7 +269,7 @@ export function UnlockAccountModal({ onSuccess, onClose }: UnlockAccountModalPro

{error &&

{error}

} - +
) diff --git a/hooks/useNotifications.ts b/hooks/useNotifications.ts index 96e7835..bab0061 100644 --- a/hooks/useNotifications.ts +++ b/hooks/useNotifications.ts @@ -48,48 +48,54 @@ export function useNotifications(userPubkey: string | null): { const unreadCount = notifications.filter((n) => !n.read).length const markAsRead = useCallback( - async (notificationId: string): Promise => { + (notificationId: string): void => { if (!userPubkey) { return } - try { - await notificationService.markAsRead(notificationId) - setNotifications((prev) => - prev.map((n) => (n.id === notificationId ? { ...n, read: true } : n)) - ) - } catch (error) { - console.error('[useNotifications] Error marking notification as read:', error) - } + void (async (): Promise => { + try { + await notificationService.markAsRead(notificationId) + setNotifications((prev) => + prev.map((n) => (n.id === notificationId ? { ...n, read: true } : n)) + ) + } catch (error) { + console.error('[useNotifications] Error marking notification as read:', error) + } + })() }, [userPubkey] ) - const markAllAsReadHandler = useCallback(async (): Promise => { + const markAllAsReadHandler = useCallback((): void => { if (!userPubkey) { return } - try { - await notificationService.markAllAsRead() - setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))) - } catch (error) { - console.error('[useNotifications] Error marking all as read:', error) - } + void (async (): Promise => { + try { + await notificationService.markAllAsRead() + setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))) + } catch (error) { + console.error('[useNotifications] Error marking all as read:', error) + } + })() }, [userPubkey]) const deleteNotificationHandler = useCallback( - async (notificationId: string): Promise => { + (notificationId: string): void => { if (!userPubkey) { return } - try { - await notificationService.deleteNotification(notificationId) - setNotifications((prev) => prev.filter((n) => n.id !== notificationId)) - } catch (error) { - console.error('[useNotifications] Error deleting notification:', error) - } + void (async (): Promise => { + try { + await notificationService.deleteNotification(notificationId) + setNotifications((prev) => prev.filter((n) => n.id !== notificationId)) + } catch (error) { + console.error('[useNotifications] Error deleting notification:', error) + } + })() }, [userPubkey] ) diff --git a/lib/articlePublisherHelpersPresentation.ts b/lib/articlePublisherHelpersPresentation.ts index 41b8658..6887a98 100644 --- a/lib/articlePublisherHelpersPresentation.ts +++ b/lib/articlePublisherHelpersPresentation.ts @@ -140,11 +140,7 @@ export async function parsePresentationEvent(event: Event): Promise + const result: { + authorName?: string + presentation?: string + contentDescription?: string + mainnetAddress?: string + pictureUrl?: string + category?: string + } = {} + + if (typeof obj.authorName === 'string') { + result.authorName = obj.authorName + } + if (typeof obj.presentation === 'string') { + result.presentation = obj.presentation + } + if (typeof obj.contentDescription === 'string') { + result.contentDescription = obj.contentDescription + } + if (typeof obj.mainnetAddress === 'string') { + result.mainnetAddress = obj.mainnetAddress + } + if (typeof obj.pictureUrl === 'string') { + result.pictureUrl = obj.pictureUrl + } + if (typeof obj.category === 'string') { + result.category = obj.category + } + + return result + } catch (error) { + console.error('Error parsing presentation profile JSON:', error) + return null + } +} + export async function fetchAuthorPresentationFromPool( pool: SimplePoolWithSub, pubkey: string diff --git a/lib/nostr.ts b/lib/nostr.ts index 4e88148..5452810 100644 --- a/lib/nostr.ts +++ b/lib/nostr.ts @@ -113,9 +113,8 @@ class NostrService { success: true, }) } else { - const error = result.reason - const errorMessage = error instanceof Error ? error.message : String(error) - console.error(`[NostrService] Relay ${relayUrl} failed during publish:`, error) + const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason) + console.error(`[NostrService] Relay ${relayUrl} failed during publish:`, result.reason) relaySessionManager.markRelayFailed(relayUrl) // Log failed publication void publishLog.logPublication({ @@ -158,9 +157,8 @@ class NostrService { success: true, }) } else { - const error = result.reason - const errorMessage = error instanceof Error ? error.message : String(error) - console.error(`[NostrService] Relay ${relayUrl} failed during publish:`, error) + const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason) + console.error(`[NostrService] Relay ${relayUrl} failed during publish:`, result.reason) relaySessionManager.markRelayFailed(relayUrl) // Log failed publication void publishLog.logPublication({ diff --git a/lib/notificationDetector.ts b/lib/notificationDetector.ts index a30d033..1e88172 100644 --- a/lib/notificationDetector.ts +++ b/lib/notificationDetector.ts @@ -150,6 +150,7 @@ class NotificationDetector { try { // Get all object types that can be published const objectTypes = ['author', 'series', 'publication', 'review', 'purchase', 'sponsoring', 'review_tip', 'payment_note'] + const oneHourAgo = Date.now() - 60 * 60 * 1000 for (const objectType of objectTypes) { try { @@ -161,35 +162,33 @@ class NotificationDetector { }) for (const obj of userObjects) { - // Check if object was recently published (published changed from false to array) - if (Array.isArray(obj.published) && obj.published.length > 0) { - // Check if we already created a notification for this - const eventId = obj.id.split(':')[1] ?? obj.id - const existing = await notificationService.getNotificationByEventId(eventId) - - if (existing?.type !== 'published') { - // Check if this is a recent change (within last 5 minutes) - // We can't track old/new state easily, so we'll create notification - // if object was published recently (created in last hour and published) - const oneHourAgo = Date.now() - 60 * 60 * 1000 - const cachedObj = obj - if (cachedObj.createdAt * 1000 > oneHourAgo) { - const relays = cachedObj.published as string[] - await notificationService.createNotification({ - type: 'published', - objectType, - objectId: cachedObj.id, - eventId, - data: { - relays, - object: obj, - title: 'Publication réussie', - message: `Votre contenu a été publié sur ${relays.length} relais`, - }, - }) - } - } + if (!Array.isArray(obj.published) || obj.published.length === 0) { + continue } + + const eventId = obj.id.split(':')[1] ?? obj.id + const existing = await notificationService.getNotificationByEventId(eventId) + if (existing?.type === 'published') { + continue + } + + 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/paymentPollingMain.ts b/lib/paymentPollingMain.ts index 3b8e85a..fba69ce 100644 --- a/lib/paymentPollingMain.ts +++ b/lib/paymentPollingMain.ts @@ -45,36 +45,33 @@ export async function sendPrivateContentAfterPayment( // Publish explicit payment note (kind 1) with project tags try { const article = await nostrService.getArticleById(articleId) - if (article) { - const payerPrivateKey = nostrService.getPrivateKey() - if (payerPrivateKey) { - // Get zap receipt to extract payment hash and other info - let paymentHash = article.paymentHash ?? articleId - if (zapReceiptId) { - const { getZapReceiptById } = await import('./zapReceiptQueries') - const zapReceipt = await getZapReceiptById(zapReceiptId) - if (zapReceipt) { - const paymentHashTag = zapReceipt.tags.find((tag) => tag[0] === 'payment_hash')?.[1] - if (paymentHashTag) { - paymentHash = paymentHashTag - } - } - } - - const category = article.category === 'author-presentation' ? undefined : (article.category === 'science-fiction' || article.category === 'scientific-research' ? article.category : undefined) - await publishPurchaseNote({ - articleId: article.id, - authorPubkey: article.pubkey, - payerPubkey: recipientPubkey, - amount, - paymentHash, - ...(zapReceiptId ? { zapReceiptId } : {}), - ...(category ? { category } : {}), - ...(article.seriesId ? { seriesId: article.seriesId } : {}), - payerPrivateKey, - }) - } + if (!article) { + return logPaymentResult(result, articleId, recipientPubkey, amount) } + + const payerPrivateKey = nostrService.getPrivateKey() + if (!payerPrivateKey) { + return logPaymentResult(result, articleId, recipientPubkey, amount) + } + + const paymentHash = await resolvePaymentHashForPurchaseNote({ + articlePaymentHash: article.paymentHash, + articleId, + zapReceiptId, + }) + const category = normalizePurchaseNoteCategory(article.category) + + await publishPurchaseNote({ + articleId: article.id, + authorPubkey: article.pubkey, + payerPubkey: recipientPubkey, + amount, + paymentHash, + ...(zapReceiptId ? { zapReceiptId } : {}), + ...(category ? { category } : {}), + ...(article.seriesId ? { seriesId: article.seriesId } : {}), + payerPrivateKey, + }) } catch (error) { console.error('Error publishing purchase note:', error) // Don't fail the payment if note publication fails @@ -85,3 +82,31 @@ export async function sendPrivateContentAfterPayment( return logPaymentResult(result, articleId, recipientPubkey, amount) } + +function normalizePurchaseNoteCategory( + category: 'author-presentation' | 'science-fiction' | 'scientific-research' | string +): 'science-fiction' | 'scientific-research' | undefined { + if (category === 'science-fiction' || category === 'scientific-research') { + return category + } + return undefined +} + +async function resolvePaymentHashForPurchaseNote(params: { + articlePaymentHash: string | undefined + articleId: string + zapReceiptId?: string +}): Promise { + let paymentHash = params.articlePaymentHash ?? params.articleId + if (!params.zapReceiptId) { + return paymentHash + } + + const { getZapReceiptById } = await import('./zapReceiptQueries') + const zapReceipt = await getZapReceiptById(params.zapReceiptId) + const paymentHashTag = zapReceipt?.tags.find((tag) => tag[0] === 'payment_hash')?.[1] + if (paymentHashTag) { + paymentHash = paymentHashTag + } + return paymentHash +} diff --git a/pages/api/nip95-upload.ts b/pages/api/nip95-upload.ts index a81c349..ee9f789 100644 --- a/pages/api/nip95-upload.ts +++ b/pages/api/nip95-upload.ts @@ -1,5 +1,6 @@ import { NextApiRequest, NextApiResponse } from 'next' import { IncomingForm, File as FormidableFile } from 'formidable' +import type { Fields, Files } from 'formidable' import FormData from 'form-data' import fs from 'fs' import https from 'https' @@ -15,8 +16,57 @@ export const config = { } interface ParseResult { - fields: Record - files: Record + fields: Fields + files: Files +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +function readString(value: unknown): string | undefined { + return typeof value === 'string' ? value : undefined +} + +function getFirstFile(files: Files, fieldName: string): FormidableFile | null { + const value = files[fieldName] + if (!value) { + return null + } + if (Array.isArray(value)) { + const first = value[0] + return first ?? null + } + return value +} + +function getFormDataHeaders(formData: FormData): http.OutgoingHttpHeaders { + const raw: unknown = formData.getHeaders() + if (!isRecord(raw)) { + return {} + } + const headers: http.OutgoingHttpHeaders = {} + for (const [key, value] of Object.entries(raw)) { + const str = readString(value) + if (str !== undefined) { + headers[key] = str + } + } + return headers +} + +function getRedirectLocation(headers: unknown): string | undefined { + if (!isRecord(headers)) { + return undefined + } + const {location} = headers + if (typeof location === 'string') { + return location + } + if (Array.isArray(location) && typeof location[0] === 'string') { + return location[0] + } + return undefined } export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise { @@ -37,13 +87,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }) const parseResult = await new Promise((resolve, reject) => { - // Cast req to any to work with formidable - NextApiRequest extends IncomingMessage - form.parse(req as any, (err, fields, files) => { + form.parse(req, (err, fields, files) => { if (err) { console.error('Formidable parse error:', err) reject(err) } else { - resolve({ fields: fields as Record, files: files as Record }) + resolve({ fields, files }) } }) }) @@ -51,7 +100,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const { files } = parseResult // Get the file from the parsed form - const fileField = files.file?.[0] + const fileField = getFirstFile(files, 'file') if (!fileField) { return res.status(400).json({ error: 'No file provided' }) } @@ -84,7 +133,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const isHttps = url.protocol === 'https:' const clientModule = isHttps ? https : http - const headers = requestFormData.getHeaders() + const headers = getFormDataHeaders(requestFormData) // Add standard headers that some endpoints require headers['Accept'] = 'application/json' @@ -113,7 +162,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }) } - const requestOptions = { + const requestOptions: http.RequestOptions = { hostname: url.hostname, port: url.port ?? (isHttps ? 443 : 80), path: url.pathname + url.search, @@ -122,11 +171,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) timeout: 30000, // 30 seconds timeout } - const proxyRequest = clientModule.request(requestOptions, (proxyResponse) => { + const proxyRequest = clientModule.request(requestOptions, (proxyResponse: http.IncomingMessage) => { // Handle redirects (301, 302, 307, 308) const statusCode = proxyResponse.statusCode ?? 500 if ((statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) && proxyResponse.headers.location) { - const {location} = proxyResponse.headers + const location = getRedirectLocation(proxyResponse.headers as unknown) + if (!location) { + reject(new Error('Redirect response missing location header')) + return + } let redirectUrl: URL try { // Handle relative and absolute URLs diff --git a/pages/author/[pubkey].tsx b/pages/author/[pubkey].tsx index e8da04a..80e9a8c 100644 --- a/pages/author/[pubkey].tsx +++ b/pages/author/[pubkey].tsx @@ -295,6 +295,9 @@ export default function AuthorPage(): React.ReactElement { } const { presentation, series, totalSponsoring, loading, error, reload } = useAuthorData(hashIdOrPubkey ?? '') + const onSeriesCreated = (): void => { + void reload() + } if (!hashIdOrPubkey) { return
@@ -320,7 +323,7 @@ export default function AuthorPage(): React.ReactElement { authorPubkey={actualAuthorPubkey} loading={loading} error={error} - onSeriesCreated={reload} + onSeriesCreated={onSeriesCreated} />