diff --git a/components/ArticleEditor.tsx b/components/ArticleEditor.tsx index 3ed486f..df357cc 100644 --- a/components/ArticleEditor.tsx +++ b/components/ArticleEditor.tsx @@ -36,7 +36,7 @@ export function ArticleEditor({ onPublishSuccess, onCancel, seriesOptions, onSel const submit = buildSubmitHandler({ publishArticle, draft, - onPublishSuccess, + ...(onPublishSuccess ? { onPublishSuccess } : {}), connect, connected, }) diff --git a/components/ConnectButton.tsx b/components/ConnectButton.tsx index 27e36d6..7104566 100644 --- a/components/ConnectButton.tsx +++ b/components/ConnectButton.tsx @@ -123,7 +123,7 @@ export function ConnectButton(): React.ReactElement { }) if (mode === 'connected') { - return + return } if (mode === 'unlock_required') { @@ -146,3 +146,12 @@ export function ConnectButton(): React.ReactElement { ) } + +function requirePubkey(pubkey: string | null): string { + if (!pubkey) { + const error = new Error('Invariant violation: pubkey is required when ConnectButton mode is "connected"') + console.error(error.message, { pubkey }) + throw error + } + return pubkey +} diff --git a/components/ReviewForm.tsx b/components/ReviewForm.tsx index e8a186d..c501207 100644 --- a/components/ReviewForm.tsx +++ b/components/ReviewForm.tsx @@ -7,7 +7,7 @@ import type { ReviewFormProps } from './reviewForms/reviewFormTypes' export function ReviewForm({ article, onSuccess, onCancel }: ReviewFormProps): React.ReactElement { const { pubkey, connect } = useNostrAuth() - const ctrl = useReviewFormController({ article, pubkey, onSuccess }) + const ctrl = useReviewFormController({ article, pubkey, ...(onSuccess ? { onSuccess } : {}) }) if (!pubkey) { return ( @@ -20,5 +20,5 @@ export function ReviewForm({ article, onSuccess, onCancel }: ReviewFormProps): R ) } - return + return } diff --git a/components/ReviewTipForm.tsx b/components/ReviewTipForm.tsx index 85c92ba..d796ad1 100644 --- a/components/ReviewTipForm.tsx +++ b/components/ReviewTipForm.tsx @@ -7,7 +7,7 @@ import type { ReviewTipFormProps } from './reviewForms/reviewFormTypes' export function ReviewTipForm({ review, article, onSuccess, onCancel }: ReviewTipFormProps): React.ReactElement { const { pubkey, connect } = useNostrAuth() - const ctrl = useReviewTipFormController({ review, article, pubkey, onSuccess }) + const ctrl = useReviewTipFormController({ review, article, pubkey, ...(onSuccess ? { onSuccess } : {}) }) if (!pubkey) { return ( @@ -19,5 +19,5 @@ export function ReviewTipForm({ review, article, onSuccess, onCancel }: ReviewTi /> ) } - return + return } diff --git a/components/reviewForms/useReviewFormController.ts b/components/reviewForms/useReviewFormController.ts index 3eed1d1..ed13b93 100644 --- a/components/reviewForms/useReviewFormController.ts +++ b/components/reviewForms/useReviewFormController.ts @@ -37,7 +37,7 @@ export function useReviewFormController(params: { setLoading, setError, reset: () => resetFields({ setContent, setTitle, setText }), - onSuccess: params.onSuccess, + ...(params.onSuccess ? { onSuccess: params.onSuccess } : {}), }) return { @@ -67,7 +67,8 @@ function buildReviewSubmitHandler(params: { }): (e: React.FormEvent) => Promise { return async (e: React.FormEvent): Promise => { e.preventDefault() - if (!params.pubkey) { + const pubkey = params.pubkey + if (!pubkey) { return } const contentError = validateRequiredContent(params.content) @@ -75,7 +76,7 @@ function buildReviewSubmitHandler(params: { params.setError(contentError) return } - await submitReview(params) + await submitReview({ ...params, pubkey }) } } diff --git a/hooks/useArticlePublishing.ts b/hooks/useArticlePublishing.ts index 719fd56..ff8cca7 100644 --- a/hooks/useArticlePublishing.ts +++ b/hooks/useArticlePublishing.ts @@ -3,6 +3,7 @@ import { articlePublisher } from '@/lib/articlePublisher' import { nostrService } from '@/lib/nostr' import type { ArticleDraft } from '@/lib/articlePublisher' import type { RelayPublishStatus } from '@/lib/publishResult' +import type { PublishedArticle } from '@/lib/articlePublisherTypes' interface UseArticlePublishingState { loading: boolean @@ -83,7 +84,7 @@ function buildPublishArticleHandler(params: { } function handlePublishResult(params: { - result: { success: boolean; relayStatuses?: RelayPublishStatus[]; articleId: string | null; error?: string | undefined } + result: PublishedArticle setSuccess: (success: boolean) => void setRelayStatuses: (statuses: RelayPublishStatus[]) => void setError: (error: string | null) => void @@ -91,7 +92,7 @@ function handlePublishResult(params: { if (params.result.success) { params.setSuccess(true) params.setRelayStatuses(params.result.relayStatuses ?? []) - return params.result.articleId + return params.result.articleId ? params.result.articleId : null } params.setError(params.result.error ?? 'Failed to publish article') diff --git a/lib/articlePublisher.ts b/lib/articlePublisher.ts index 6a0206a..5b2aaba 100644 --- a/lib/articlePublisher.ts +++ b/lib/articlePublisher.ts @@ -77,7 +77,13 @@ export class ArticlePublisher { return buildFailure('Presentation not found') } - return encryptAndPublish(draft, authorPubkey, validation.authorPrivateKeyForEncryption, validation.category, presentation.id) + return encryptAndPublish({ + draft, + authorPubkey, + authorPrivateKeyForEncryption: validation.authorPrivateKeyForEncryption, + category: validation.category, + presentationId: presentation.id, + }) } catch (error) { console.error('Error publishing article:', error) return buildFailure(error instanceof Error ? error.message : 'Unknown error') diff --git a/lib/articlePublisherHelpersVerification.ts b/lib/articlePublisherHelpersVerification.ts index d960d6b..3c26bf1 100644 --- a/lib/articlePublisherHelpersVerification.ts +++ b/lib/articlePublisherHelpersVerification.ts @@ -23,77 +23,78 @@ export function createMessageVerificationFilters(messageEventId: string, authorP ] } +interface MessageVerificationContext { + messageEventId: string + articleId: string + recipientPubkey: string + authorPubkey: string +} + export function handleMessageVerificationEvent( event: import('nostr-tools').Event, - articleId: string, - recipientPubkey: string, - authorPubkey: string, + ctx: MessageVerificationContext, finalize: (value: boolean) => void ): void { console.warn('Private message verified on relay', { messageEventId: event.id, - articleId, - recipientPubkey, - authorPubkey, + articleId: ctx.articleId, + recipientPubkey: ctx.recipientPubkey, + authorPubkey: ctx.authorPubkey, timestamp: new Date().toISOString(), }) finalize(true) } export function setupMessageVerificationHandlers( - sub: import('@/types/nostr-tools-extended').Subscription, - messageEventId: string, - articleId: string, - recipientPubkey: string, - authorPubkey: string, - finalize: (value: boolean) => void, - isResolved: () => boolean + params: { + sub: import('@/types/nostr-tools-extended').Subscription + ctx: MessageVerificationContext + finalize: (value: boolean) => void + isResolved: () => boolean + } ): void { - sub.on('event', (event: Event): void => { - handleMessageVerificationEvent(event, articleId, recipientPubkey, authorPubkey, finalize) + params.sub.on('event', (event: Event): void => { + handleMessageVerificationEvent(event, params.ctx, params.finalize) }) - sub.on('eose', (): void => { + params.sub.on('eose', (): void => { console.warn('Private message not found on relay after EOSE', { - messageEventId, - articleId, - recipientPubkey, + messageEventId: params.ctx.messageEventId, + articleId: params.ctx.articleId, + recipientPubkey: params.ctx.recipientPubkey, timestamp: new Date().toISOString(), }) - finalize(false) + params.finalize(false) }) setTimeout(() => { - if (!isResolved()) { + if (!params.isResolved()) { console.warn('Timeout verifying private message on relay', { - messageEventId, - articleId, - recipientPubkey, + messageEventId: params.ctx.messageEventId, + articleId: params.ctx.articleId, + recipientPubkey: params.ctx.recipientPubkey, timestamp: new Date().toISOString(), }) - finalize(false) + params.finalize(false) } }, 5000) } function createMessageVerificationSubscription( - pool: import('nostr-tools').SimplePool, - messageEventId: string, - authorPubkey: string, - recipientPubkey: string, - articleId: string + params: { pool: import('nostr-tools').SimplePool; ctx: MessageVerificationContext } ): ReturnType { - const filters = createMessageVerificationFilters(messageEventId, authorPubkey, recipientPubkey, articleId) + const filters = createMessageVerificationFilters( + params.ctx.messageEventId, + params.ctx.authorPubkey, + params.ctx.recipientPubkey, + params.ctx.articleId + ) const relayUrl = getPrimaryRelaySync() - return createSubscription(pool, [relayUrl], filters) + return createSubscription(params.pool, [relayUrl], filters) } function createVerificationPromise( - sub: import('@/types/nostr-tools-extended').Subscription, - messageEventId: string, - articleId: string, - recipientPubkey: string, - authorPubkey: string + params: { sub: import('@/types/nostr-tools-extended').Subscription; ctx: MessageVerificationContext } ): Promise { return new Promise((resolve) => { let resolved = false @@ -103,11 +104,11 @@ function createVerificationPromise( return } resolved = true - sub.unsub() + params.sub.unsub() resolve(value) } - setupMessageVerificationHandlers(sub, messageEventId, articleId, recipientPubkey, authorPubkey, finalize, () => resolved) + setupMessageVerificationHandlers({ sub: params.sub, ctx: params.ctx, finalize, isResolved: () => resolved }) }) } @@ -128,15 +129,9 @@ export function verifyPrivateMessagePublished( return Promise.resolve(false) } - const sub = createMessageVerificationSubscription( - pool, - messageEventId, - authorPubkey, - recipientPubkey, - articleId - ) - - return createVerificationPromise(sub, messageEventId, articleId, recipientPubkey, authorPubkey) + const ctx: MessageVerificationContext = { messageEventId, articleId, recipientPubkey, authorPubkey } + const sub = createMessageVerificationSubscription({ pool, ctx }) + return createVerificationPromise({ sub, ctx }) } catch (error) { console.error('Error verifying private message', { messageEventId, diff --git a/lib/articlePublisherPublish.ts b/lib/articlePublisherPublish.ts index 683e7c3..10bee44 100644 --- a/lib/articlePublisherPublish.ts +++ b/lib/articlePublisherPublish.ts @@ -77,16 +77,21 @@ async function buildParsedArticleFromDraft( return { article, hash, version, index } } -export async function publishPreview( - draft: ArticleDraft, - invoice: AlbyInvoice, - authorPubkey: string, - presentationId: string, - extraTags?: string[][], - encryptedContent?: string, - encryptedKey?: string, +interface PublishPreviewParams { + draft: ArticleDraft + invoice: AlbyInvoice + authorPubkey: string + presentationId: string + extraTags?: string[][] + encryptedContent?: string + encryptedKey?: string returnStatus?: boolean +} + +export async function publishPreview( + params: PublishPreviewParams ): Promise { + const { draft, invoice, authorPubkey, presentationId, extraTags, encryptedContent, encryptedKey, returnStatus } = params // Build parsed article object const { article, hash, version, index } = await buildParsedArticleFromDraft(draft, invoice, authorPubkey) @@ -158,17 +163,29 @@ export function buildArticleExtraTags(draft: ArticleDraft, _category: NonNullabl } export async function encryptAndPublish( - draft: ArticleDraft, - authorPubkey: string, - authorPrivateKeyForEncryption: string, - category: NonNullable, - presentationId: string + params: { + draft: ArticleDraft + authorPubkey: string + authorPrivateKeyForEncryption: string + category: NonNullable + presentationId: string + } ): Promise { + const { draft, authorPubkey, authorPrivateKeyForEncryption, category, presentationId } = params const { encryptedContent, key, iv } = await encryptArticleContent(draft.content) const encryptedKey = await encryptDecryptionKey(key, iv, authorPrivateKeyForEncryption, authorPubkey) const invoice = await createArticleInvoice(draft) const extraTags = buildArticleExtraTags(draft, category) - const publishResult = await publishPreview(draft, invoice, authorPubkey, presentationId, extraTags, encryptedContent, encryptedKey, true) + const publishResult = await publishPreview({ + draft, + invoice, + authorPubkey, + presentationId, + extraTags, + encryptedContent, + encryptedKey, + returnStatus: true, + }) if (!publishResult) { return buildFailure('Failed to publish article')