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}
/>