lint fix wip

This commit is contained in:
Nicolas Cantu 2026-01-09 08:45:54 +01:00
parent 9e3d2c8742
commit 7c0dc68304
11 changed files with 268 additions and 122 deletions

View File

@ -60,10 +60,14 @@ export function LanguageSelector(): React.ReactElement {
window.location.reload()
}
const onLocaleClick = (locale: Locale): void => {
void handleLocaleChange(locale)
}
return (
<div className="flex items-center gap-2">
<LocaleButton locale="fr" label="FR" currentLocale={currentLocale} onClick={handleLocaleChange} />
<LocaleButton locale="en" label="EN" currentLocale={currentLocale} onClick={handleLocaleChange} />
<LocaleButton locale="fr" label="FR" currentLocale={currentLocale} onClick={onLocaleClick} />
<LocaleButton locale="en" label="EN" currentLocale={currentLocale} onClick={onLocaleClick} />
</div>
)
}

View File

@ -63,6 +63,10 @@ export function LanguageSettingsManager(): React.ReactElement {
window.location.reload()
}
const onLocaleClick = (locale: Locale): void => {
void handleLocaleChange(locale)
}
if (loading) {
return (
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
@ -76,8 +80,8 @@ export function LanguageSettingsManager(): React.ReactElement {
<h2 className="text-2xl font-bold text-neon-cyan mb-4">{t('settings.language.title')}</h2>
<p className="text-cyber-accent mb-4 text-sm">{t('settings.language.description')}</p>
<div className="flex items-center gap-3">
<LocaleOption locale="fr" label={t('settings.language.french')} currentLocale={currentLocale} onClick={handleLocaleChange} />
<LocaleOption locale="en" label={t('settings.language.english')} currentLocale={currentLocale} onClick={handleLocaleChange} />
<LocaleOption locale="fr" label={t('settings.language.french')} currentLocale={currentLocale} onClick={onLocaleClick} />
<LocaleOption locale="en" label={t('settings.language.english')} currentLocale={currentLocale} onClick={onLocaleClick} />
</div>
</div>
)

View File

@ -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)}`}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3 flex-1">
@ -377,3 +371,13 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps):
</div>
)
}
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'
}

View File

@ -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<void>
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<void> => {
const handlePasteAsync = async (): Promise<void> => {
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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
@ -261,7 +269,7 @@ export function UnlockAccountModal({ onSuccess, onClose }: UnlockAccountModalPro
</p>
<UnlockAccountForm words={words} handleWordChange={handleWordChange} handlePaste={handlePaste} />
{error && <p className="text-sm text-red-600 mb-4">{error}</p>}
<UnlockAccountButtons loading={loading} words={words} onUnlock={handleUnlock} onClose={onClose} />
<UnlockAccountButtons loading={loading} words={words} onUnlock={onUnlock} onClose={onClose} />
</div>
</div>
)

View File

@ -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<void> => {
(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<void> => {
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<void> => {
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<void> => {
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<void> => {
(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<void> => {
try {
await notificationService.deleteNotification(notificationId)
setNotifications((prev) => prev.filter((n) => n.id !== notificationId))
} catch (error) {
console.error('[useNotifications] Error deleting notification:', error)
}
})()
},
[userPubkey]
)

View File

@ -140,11 +140,7 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
} | null = null
if (tags.json) {
try {
profileData = JSON.parse(tags.json)
} catch (jsonError) {
console.error('Error parsing JSON from tag:', jsonError)
}
profileData = parsePresentationProfileJson(tags.json)
}
// Fallback to content format (for backward compatibility with old notes)
@ -155,7 +151,7 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
try {
// Remove zero-width characters from JSON
const cleanedJson = invisibleJsonMatch[1].replace(/[\u200B\u200C\u200D\u200E\u200F]/g, '').trim()
profileData = JSON.parse(cleanedJson)
profileData = parsePresentationProfileJson(cleanedJson)
} catch (invisibleJsonError) {
console.error('Error parsing profile JSON from invisible content:', invisibleJsonError)
}
@ -165,11 +161,7 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
if (!profileData) {
const jsonMatch = event.content.match(/\[Metadata JSON\]\n(.+)$/s)
if (jsonMatch?.[1]) {
try {
profileData = JSON.parse(jsonMatch[1].trim())
} catch (contentJsonError) {
console.error('Error parsing profile JSON from content:', contentJsonError)
}
profileData = parsePresentationProfileJson(jsonMatch[1].trim())
}
}
}
@ -278,6 +270,56 @@ export async function parsePresentationEvent(event: Event): Promise<import('@/ty
return result
}
function parsePresentationProfileJson(json: string): {
authorName?: string
presentation?: string
contentDescription?: string
mainnetAddress?: string
pictureUrl?: string
category?: string
} | null {
try {
const parsed: unknown = JSON.parse(json)
if (typeof parsed !== 'object' || parsed === null) {
return null
}
const obj = parsed as Record<string, unknown>
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

View File

@ -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({

View File

@ -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)

View File

@ -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<string> {
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
}

View File

@ -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<string, string[]>
files: Record<string, FormidableFile[]>
fields: Fields
files: Files
}
function isRecord(value: unknown): value is Record<string, unknown> {
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<void> {
@ -37,13 +87,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
})
const parseResult = await new Promise<ParseResult>((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<string, string[]>, files: files as Record<string, FormidableFile[]> })
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

View File

@ -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 <div />
@ -320,7 +323,7 @@ export default function AuthorPage(): React.ReactElement {
authorPubkey={actualAuthorPubkey}
loading={loading}
error={error}
onSeriesCreated={reload}
onSeriesCreated={onSeriesCreated}
/>
</div>
<Footer />