From 9ad602d1008b616f49ed575d55139c37e0a1c56c Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Tue, 13 Jan 2026 14:49:19 +0100 Subject: [PATCH] lint wip --- components/AuthorPresentationEditor.tsx | 594 +------------- components/KeyManagementManager.tsx | 560 +------------ components/MarkdownEditorTwoColumns.tsx | 380 +-------- components/Nip95ConfigManager.tsx | 368 +-------- components/RelayManager.tsx | 413 +--------- components/SponsoringForm.tsx | 309 ++++--- components/SyncProgressBar.tsx | 347 +------- components/UnlockAccountModal.tsx | 275 +------ components/UserArticles.tsx | 326 +------- .../AuthorPresentationEditor.tsx | 80 ++ .../NoAccountView.tsx | 147 ++++ .../PresentationForm.tsx | 75 ++ .../authorPresentationEditor/fields.tsx | 88 ++ components/authorPresentationEditor/types.ts | 7 + .../useAuthorPresentationState.ts | 124 +++ .../useExistingPresentation.ts | 40 + .../authorPresentationEditor/validation.ts | 15 + .../keyManagement/KeyManagementImportForm.tsx | 130 +++ .../KeyManagementImportSection.tsx | 125 +++ .../keyManagement/KeyManagementManager.tsx | 28 + .../KeyManagementRecoverySection.tsx | 98 +++ components/keyManagement/keyImportParsing.ts | 53 ++ .../keyManagement/keyManagementController.ts | 154 ++++ components/keyManagement/types.ts | 4 + .../keyManagement/useKeyManagementManager.ts | 140 ++++ .../MarkdownEditorTwoColumns.tsx | 158 ++++ .../markdownEditorTwoColumns/PagesManager.tsx | 165 ++++ .../markdownEditorTwoColumns/imageUpload.ts | 32 + components/nip95Config/Nip95ApiCard.tsx | 155 ++++ components/nip95Config/Nip95ApiList.tsx | 30 + components/nip95Config/Nip95ConfigContent.tsx | 107 +++ components/nip95Config/Nip95ConfigManager.tsx | 45 ++ components/nip95Config/Nip95ConfigView.tsx | 37 + components/nip95Config/controller.ts | 212 +++++ components/nip95Config/getApiCardClassName.ts | 9 + components/nip95Config/view.tsx | 1 + components/nip95Config/viewModel.ts | 121 +++ components/relayManager/RelayCard.tsx | 132 +++ components/relayManager/RelayList.tsx | 49 ++ components/relayManager/RelayManager.tsx | 46 ++ .../relayManager/RelayManagerContent.tsx | 100 +++ components/relayManager/controller.ts | 256 ++++++ components/relayManager/types.ts | 29 + components/relayManager/view.tsx | 14 + components/relayManager/viewModel.ts | 6 + components/relayManager/viewProps.ts | 130 +++ .../syncProgressBar/SyncProgressBar.tsx | 22 + components/syncProgressBar/controller.ts | 216 +++++ components/syncProgressBar/types.ts | 16 + components/syncProgressBar/view.tsx | 73 ++ .../unlockAccount/UnlockAccountButtons.tsx | 26 + .../unlockAccount/UnlockAccountForm.tsx | 45 ++ .../unlockAccount/UnlockAccountModal.tsx | 35 + .../WordInputWithAutocomplete.tsx | 52 ++ components/unlockAccount/WordSuggestions.tsx | 24 + .../unlockAccount/autocompleteKeyDecision.ts | 27 + components/unlockAccount/types.ts | 4 + .../useUnlockAccountController.ts | 92 +++ .../unlockAccount/useWordAutocomplete.ts | 108 +++ components/userArticles/UserArticles.tsx | 17 + components/userArticles/controller.ts | 101 +++ components/userArticles/layout.tsx | 93 +++ components/userArticles/types.ts | 45 ++ hooks/useArticlePayment.ts | 43 +- lib/articleMutations.ts | 674 +--------------- lib/articleMutations/delete.ts | 85 ++ lib/articleMutations/index.ts | 5 + lib/articleMutations/publicationPreview.ts | 57 ++ lib/articleMutations/review.ts | 133 ++++ lib/articleMutations/series.ts | 143 ++++ lib/articleMutations/shared.ts | 40 + lib/articleMutations/types.ts | 5 + lib/articleMutations/update.ts | 100 +++ lib/articlePublisher.ts | 121 +-- lib/articlePublisherHelpersPresentation.ts | 521 +----------- .../articleBuilder.ts | 102 +++ .../buildPresentationEvent.ts | 148 ++++ .../fetchAuthorPresentationFromPool.ts | 122 +++ .../idResolution.ts | 97 +++ .../parsePresentationEvent.ts | 18 + .../profileData.ts | 26 + .../profileJson.ts | 38 + lib/articlePublisherPresentationHelpers.ts | 91 +++ lib/helpers/indexedDBHelper.ts | 617 +------------- lib/helpers/indexedDBHelper/IndexedDBError.ts | 15 + .../indexedDBHelper/IndexedDBHelper.ts | 97 +++ lib/helpers/indexedDBHelper/core.ts | 97 +++ lib/helpers/indexedDBHelper/cursor.ts | 77 ++ lib/helpers/indexedDBHelper/index.ts | 10 + lib/helpers/indexedDBHelper/read.ts | 67 ++ lib/helpers/indexedDBHelper/store.ts | 14 + lib/helpers/indexedDBHelper/types.ts | 14 + lib/helpers/indexedDBHelper/write.ts | 58 ++ lib/hooks/useSyncProgress.ts | 154 ++-- lib/keyManagementTwoLevel.ts | 440 +--------- lib/keyManagementTwoLevel/api.ts | 94 +++ lib/keyManagementTwoLevel/credentials.ts | 51 ++ lib/keyManagementTwoLevel/crypto.ts | 60 ++ lib/keyManagementTwoLevel/encoding.ts | 30 + lib/keyManagementTwoLevel/kekEncryption.ts | 20 + .../privateKeyEncryption.ts | 18 + lib/markdownRenderer.tsx | 19 +- lib/metadataExtractor.ts | 752 +----------------- lib/metadataExtractor/author.ts | 79 ++ lib/metadataExtractor/index.ts | 8 + lib/metadataExtractor/metadataJson.ts | 46 ++ lib/metadataExtractor/publication.ts | 109 +++ lib/metadataExtractor/purchase.ts | 23 + lib/metadataExtractor/review.ts | 93 +++ lib/metadataExtractor/reviewTip.ts | 40 + lib/metadataExtractor/series.ts | 73 ++ lib/metadataExtractor/sponsoring.ts | 48 ++ lib/metadataExtractor/types.ts | 99 +++ lib/metadataExtractor/utils.ts | 68 ++ lib/nostr.ts | 502 +----------- lib/nostr/articles.ts | 35 + lib/nostr/cache.ts | 35 + lib/nostr/decryption.ts | 74 ++ lib/nostr/publish.ts | 68 ++ lib/nostr/publishedStatus.ts | 69 ++ lib/nostr/service.ts | 182 +++++ lib/nostrEventParsing.ts | 508 +----------- lib/nostrEventParsing/article.ts | 147 ++++ lib/nostrEventParsing/index.ts | 4 + lib/nostrEventParsing/review.ts | 66 ++ lib/nostrEventParsing/series.ts | 62 ++ lib/nostrEventParsing/shared.ts | 101 +++ lib/nostrEventParsing/zapReceipts.ts | 108 +++ lib/objectCache.ts | 449 +---------- lib/objectCache/ObjectCacheService.ts | 268 +++++++ lib/objectCache/db.ts | 57 ++ lib/objectCache/index.ts | 2 + lib/objectCache/objectCache.ts | 3 + lib/objectCache/types.ts | 25 + lib/paymentNotes.ts | 484 +---------- lib/paymentNotes/index.ts | 3 + lib/paymentNotes/purchase.ts | 114 +++ lib/paymentNotes/reviewTip.ts | 139 ++++ lib/paymentNotes/shared.ts | 38 + lib/paymentNotes/sponsoring.ts | 162 ++++ lib/platformSync.ts | 625 +-------------- lib/platformSync/PlatformSyncService.ts | 198 +++++ lib/platformSync/cacheEvent.ts | 216 +++++ lib/platformSync/constants.ts | 4 + lib/platformSync/debug.ts | 117 +++ lib/platformSync/index.ts | 5 + lib/platformSync/relayCollection.ts | 188 +++++ lib/writeService.ts | 429 +--------- lib/writeService/WriteService.ts | 226 ++++++ .../createNotificationDecision.ts | 32 + lib/writeService/direct.ts | 41 + lib/writeService/env.ts | 4 + lib/writeService/index.ts | 5 + lib/writeService/notificationDecision.ts | 33 + lib/writeService/objectWriteOps.ts | 111 +++ lib/writeService/payloads.ts | 36 + lib/writeService/singleton.ts | 4 + lib/writeService/types.ts | 42 + lib/writeService/workerInit.ts | 63 ++ lib/writeService/workerMessage.ts | 31 + lib/writeService/workerPayloads.ts | 25 + pages/_app.tsx | 47 +- pages/api/nip95-upload.ts | 536 ------------- pages/api/nip95-upload/handler.ts | 37 + pages/api/nip95-upload/index.ts | 11 + pages/api/nip95-upload/multipart.ts | 43 + pages/api/nip95-upload/proxyRequest.ts | 144 ++++ pages/api/nip95-upload/proxyRequestErrors.ts | 18 + .../api/nip95-upload/requestErrorResponse.ts | 33 + pages/api/nip95-upload/responseFormatting.ts | 133 ++++ pages/api/nip95-upload/systemErrors.ts | 8 + pages/api/nip95-upload/types.ts | 26 + pages/api/nip95-upload/utils.ts | 39 + pages/author/[pubkey].tsx | 206 ++--- pages/docs.tsx | 38 +- pages/index.tsx | 30 +- pages/purchase/[id].tsx | 88 +- pages/review-tip/[id].tsx | 100 ++- pages/series/[id]/publish.tsx | 104 ++- pages/sponsoring/[id].tsx | 102 +-- types/nostr-tools-extended.ts | 86 +- 181 files changed, 11140 insertions(+), 10498 deletions(-) create mode 100644 components/authorPresentationEditor/AuthorPresentationEditor.tsx create mode 100644 components/authorPresentationEditor/NoAccountView.tsx create mode 100644 components/authorPresentationEditor/PresentationForm.tsx create mode 100644 components/authorPresentationEditor/fields.tsx create mode 100644 components/authorPresentationEditor/types.ts create mode 100644 components/authorPresentationEditor/useAuthorPresentationState.ts create mode 100644 components/authorPresentationEditor/useExistingPresentation.ts create mode 100644 components/authorPresentationEditor/validation.ts create mode 100644 components/keyManagement/KeyManagementImportForm.tsx create mode 100644 components/keyManagement/KeyManagementImportSection.tsx create mode 100644 components/keyManagement/KeyManagementManager.tsx create mode 100644 components/keyManagement/KeyManagementRecoverySection.tsx create mode 100644 components/keyManagement/keyImportParsing.ts create mode 100644 components/keyManagement/keyManagementController.ts create mode 100644 components/keyManagement/types.ts create mode 100644 components/keyManagement/useKeyManagementManager.ts create mode 100644 components/markdownEditorTwoColumns/MarkdownEditorTwoColumns.tsx create mode 100644 components/markdownEditorTwoColumns/PagesManager.tsx create mode 100644 components/markdownEditorTwoColumns/imageUpload.ts create mode 100644 components/nip95Config/Nip95ApiCard.tsx create mode 100644 components/nip95Config/Nip95ApiList.tsx create mode 100644 components/nip95Config/Nip95ConfigContent.tsx create mode 100644 components/nip95Config/Nip95ConfigManager.tsx create mode 100644 components/nip95Config/Nip95ConfigView.tsx create mode 100644 components/nip95Config/controller.ts create mode 100644 components/nip95Config/getApiCardClassName.ts create mode 100644 components/nip95Config/view.tsx create mode 100644 components/nip95Config/viewModel.ts create mode 100644 components/relayManager/RelayCard.tsx create mode 100644 components/relayManager/RelayList.tsx create mode 100644 components/relayManager/RelayManager.tsx create mode 100644 components/relayManager/RelayManagerContent.tsx create mode 100644 components/relayManager/controller.ts create mode 100644 components/relayManager/types.ts create mode 100644 components/relayManager/view.tsx create mode 100644 components/relayManager/viewModel.ts create mode 100644 components/relayManager/viewProps.ts create mode 100644 components/syncProgressBar/SyncProgressBar.tsx create mode 100644 components/syncProgressBar/controller.ts create mode 100644 components/syncProgressBar/types.ts create mode 100644 components/syncProgressBar/view.tsx create mode 100644 components/unlockAccount/UnlockAccountButtons.tsx create mode 100644 components/unlockAccount/UnlockAccountForm.tsx create mode 100644 components/unlockAccount/UnlockAccountModal.tsx create mode 100644 components/unlockAccount/WordInputWithAutocomplete.tsx create mode 100644 components/unlockAccount/WordSuggestions.tsx create mode 100644 components/unlockAccount/autocompleteKeyDecision.ts create mode 100644 components/unlockAccount/types.ts create mode 100644 components/unlockAccount/useUnlockAccountController.ts create mode 100644 components/unlockAccount/useWordAutocomplete.ts create mode 100644 components/userArticles/UserArticles.tsx create mode 100644 components/userArticles/controller.ts create mode 100644 components/userArticles/layout.tsx create mode 100644 components/userArticles/types.ts create mode 100644 lib/articleMutations/delete.ts create mode 100644 lib/articleMutations/index.ts create mode 100644 lib/articleMutations/publicationPreview.ts create mode 100644 lib/articleMutations/review.ts create mode 100644 lib/articleMutations/series.ts create mode 100644 lib/articleMutations/shared.ts create mode 100644 lib/articleMutations/types.ts create mode 100644 lib/articleMutations/update.ts create mode 100644 lib/articlePublisherHelpersPresentation/articleBuilder.ts create mode 100644 lib/articlePublisherHelpersPresentation/buildPresentationEvent.ts create mode 100644 lib/articlePublisherHelpersPresentation/fetchAuthorPresentationFromPool.ts create mode 100644 lib/articlePublisherHelpersPresentation/idResolution.ts create mode 100644 lib/articlePublisherHelpersPresentation/parsePresentationEvent.ts create mode 100644 lib/articlePublisherHelpersPresentation/profileData.ts create mode 100644 lib/articlePublisherHelpersPresentation/profileJson.ts create mode 100644 lib/articlePublisherPresentationHelpers.ts create mode 100644 lib/helpers/indexedDBHelper/IndexedDBError.ts create mode 100644 lib/helpers/indexedDBHelper/IndexedDBHelper.ts create mode 100644 lib/helpers/indexedDBHelper/core.ts create mode 100644 lib/helpers/indexedDBHelper/cursor.ts create mode 100644 lib/helpers/indexedDBHelper/index.ts create mode 100644 lib/helpers/indexedDBHelper/read.ts create mode 100644 lib/helpers/indexedDBHelper/store.ts create mode 100644 lib/helpers/indexedDBHelper/types.ts create mode 100644 lib/helpers/indexedDBHelper/write.ts create mode 100644 lib/keyManagementTwoLevel/api.ts create mode 100644 lib/keyManagementTwoLevel/credentials.ts create mode 100644 lib/keyManagementTwoLevel/crypto.ts create mode 100644 lib/keyManagementTwoLevel/encoding.ts create mode 100644 lib/keyManagementTwoLevel/kekEncryption.ts create mode 100644 lib/keyManagementTwoLevel/privateKeyEncryption.ts create mode 100644 lib/metadataExtractor/author.ts create mode 100644 lib/metadataExtractor/index.ts create mode 100644 lib/metadataExtractor/metadataJson.ts create mode 100644 lib/metadataExtractor/publication.ts create mode 100644 lib/metadataExtractor/purchase.ts create mode 100644 lib/metadataExtractor/review.ts create mode 100644 lib/metadataExtractor/reviewTip.ts create mode 100644 lib/metadataExtractor/series.ts create mode 100644 lib/metadataExtractor/sponsoring.ts create mode 100644 lib/metadataExtractor/types.ts create mode 100644 lib/metadataExtractor/utils.ts create mode 100644 lib/nostr/articles.ts create mode 100644 lib/nostr/cache.ts create mode 100644 lib/nostr/decryption.ts create mode 100644 lib/nostr/publish.ts create mode 100644 lib/nostr/publishedStatus.ts create mode 100644 lib/nostr/service.ts create mode 100644 lib/nostrEventParsing/article.ts create mode 100644 lib/nostrEventParsing/index.ts create mode 100644 lib/nostrEventParsing/review.ts create mode 100644 lib/nostrEventParsing/series.ts create mode 100644 lib/nostrEventParsing/shared.ts create mode 100644 lib/nostrEventParsing/zapReceipts.ts create mode 100644 lib/objectCache/ObjectCacheService.ts create mode 100644 lib/objectCache/db.ts create mode 100644 lib/objectCache/index.ts create mode 100644 lib/objectCache/objectCache.ts create mode 100644 lib/objectCache/types.ts create mode 100644 lib/paymentNotes/index.ts create mode 100644 lib/paymentNotes/purchase.ts create mode 100644 lib/paymentNotes/reviewTip.ts create mode 100644 lib/paymentNotes/shared.ts create mode 100644 lib/paymentNotes/sponsoring.ts create mode 100644 lib/platformSync/PlatformSyncService.ts create mode 100644 lib/platformSync/cacheEvent.ts create mode 100644 lib/platformSync/constants.ts create mode 100644 lib/platformSync/debug.ts create mode 100644 lib/platformSync/index.ts create mode 100644 lib/platformSync/relayCollection.ts create mode 100644 lib/writeService/WriteService.ts create mode 100644 lib/writeService/createNotificationDecision.ts create mode 100644 lib/writeService/direct.ts create mode 100644 lib/writeService/env.ts create mode 100644 lib/writeService/index.ts create mode 100644 lib/writeService/notificationDecision.ts create mode 100644 lib/writeService/objectWriteOps.ts create mode 100644 lib/writeService/payloads.ts create mode 100644 lib/writeService/singleton.ts create mode 100644 lib/writeService/types.ts create mode 100644 lib/writeService/workerInit.ts create mode 100644 lib/writeService/workerMessage.ts create mode 100644 lib/writeService/workerPayloads.ts delete mode 100644 pages/api/nip95-upload.ts create mode 100644 pages/api/nip95-upload/handler.ts create mode 100644 pages/api/nip95-upload/index.ts create mode 100644 pages/api/nip95-upload/multipart.ts create mode 100644 pages/api/nip95-upload/proxyRequest.ts create mode 100644 pages/api/nip95-upload/proxyRequestErrors.ts create mode 100644 pages/api/nip95-upload/requestErrorResponse.ts create mode 100644 pages/api/nip95-upload/responseFormatting.ts create mode 100644 pages/api/nip95-upload/systemErrors.ts create mode 100644 pages/api/nip95-upload/types.ts create mode 100644 pages/api/nip95-upload/utils.ts diff --git a/components/AuthorPresentationEditor.tsx b/components/AuthorPresentationEditor.tsx index 5213f2d..6ce2885 100644 --- a/components/AuthorPresentationEditor.tsx +++ b/components/AuthorPresentationEditor.tsx @@ -1,593 +1 @@ -import { useState, useCallback, useEffect, type FormEvent } from 'react' -import { useRouter } from 'next/router' -import { useNostrAuth } from '@/hooks/useNostrAuth' -import { useAuthorPresentation } from '@/hooks/useAuthorPresentation' -import { ArticleField } from './ArticleField' -import { CreateAccountModal } from './CreateAccountModal' -import { RecoveryStep } from './CreateAccountModalSteps' -import { UnlockAccountModal } from './UnlockAccountModal' -import { ImageUploadField } from './ImageUploadField' -import { PresentationFormHeader } from './PresentationFormHeader' -import { extractPresentationData } from '@/lib/presentationParsing' -import type { Article } from '@/types/nostr' -import { t } from '@/lib/i18n' -import { userConfirm } from '@/lib/userConfirm' - -interface AuthorPresentationDraft { - authorName: string - presentation: string - contentDescription: string - mainnetAddress: string - pictureUrl?: string -} - -const ADDRESS_PATTERN = /^(1|3|bc1)[a-zA-Z0-9]{25,62}$/ - -function SuccessNotice({ pubkey }: { pubkey: string | null }): React.ReactElement { - return ( -
-

{t('presentation.success')}

-

- {t('presentation.successMessage')} -

- {pubkey && ( -
- - {t('presentation.manageSeries')} - -
- )} -
- ) -} - -function ValidationError({ message }: { message: string | null }): React.ReactElement | null { - if (!message) { - return null - } - return ( -
-

{message}

-
- ) -} - -function PresentationField({ - draft, - onChange, -}: { - draft: AuthorPresentationDraft - onChange: (next: AuthorPresentationDraft) => void -}): React.ReactElement { - return ( - onChange({ ...draft, presentation: value as string })} - required - type="textarea" - rows={6} - placeholder={t('presentation.field.presentation.placeholder')} - helpText={t('presentation.field.presentation.help')} - /> - ) -} - -function ContentDescriptionField({ - draft, - onChange, -}: { - draft: AuthorPresentationDraft - onChange: (next: AuthorPresentationDraft) => void -}): React.ReactElement { - return ( - onChange({ ...draft, contentDescription: value as string })} - required - type="textarea" - rows={6} - placeholder={t('presentation.field.contentDescription.placeholder')} - helpText={t('presentation.field.contentDescription.help')} - /> - ) -} - -function MainnetAddressField({ - draft, - onChange, -}: { - draft: AuthorPresentationDraft - onChange: (next: AuthorPresentationDraft) => void -}): React.ReactElement { - return ( - onChange({ ...draft, mainnetAddress: value as string })} - required - type="text" - placeholder={t('presentation.field.mainnetAddress.placeholder')} - helpText={t('presentation.field.mainnetAddress.help')} - /> - ) -} - -function AuthorNameField({ - draft, - onChange, -}: { - draft: AuthorPresentationDraft - onChange: (next: AuthorPresentationDraft) => void -}): React.ReactElement { - return ( - onChange({ ...draft, authorName: value as string })} - required - type="text" - placeholder={t('presentation.field.authorName.placeholder')} - helpText={t('presentation.field.authorName.help')} - /> - ) -} - -function PictureField({ - draft, - onChange, -}: { - draft: AuthorPresentationDraft - onChange: (next: AuthorPresentationDraft) => void -}): React.ReactElement { - return ( - onChange({ ...draft, pictureUrl: url })} - /> - ) -} - -const PresentationFields = ({ - draft, - onChange, -}: { - draft: AuthorPresentationDraft - onChange: (next: AuthorPresentationDraft) => void -}): React.ReactElement => ( -
- - - - - -
-) - -function DeleteButton({ onDelete, deleting }: { onDelete: () => void; deleting: boolean }): React.ReactElement { - return ( - - ) -} - -type PresentationFormProps = { - draft: AuthorPresentationDraft - setDraft: (next: AuthorPresentationDraft) => void - validationError: string | null - error: string | null - loading: boolean - handleSubmit: (e: FormEvent) => Promise - deleting: boolean - handleDelete: () => void - hasExistingPresentation: boolean -} - -function PresentationForm(props: PresentationFormProps): React.ReactElement { - return ( -
) => { - void props.handleSubmit(e) - }} - className="border border-neon-cyan/20 rounded-lg p-6 bg-cyber-dark space-y-4" - > - - - -
-
- -
- {props.hasExistingPresentation && ( - { void props.handleDelete() }} deleting={props.deleting} /> - )} -
- - ) -} - -function getSubmitLabel(params: { loading: boolean; deleting: boolean; hasExistingPresentation: boolean }): string { - if (params.loading || params.deleting) { - return t('publish.publishing') - } - return params.hasExistingPresentation ? t('presentation.update.button') : t('publish.button') -} - -function useAuthorPresentationState(pubkey: string | null, existingAuthorName?: string, existingPresentation?: Article | null): { - draft: AuthorPresentationDraft - setDraft: (next: AuthorPresentationDraft) => void - validationError: string | null - error: string | null - loading: boolean - handleSubmit: (e: FormEvent) => Promise - deleting: boolean - handleDelete: () => Promise - success: boolean -} { - const { loading, error, success, publishPresentation, deletePresentation } = useAuthorPresentation(pubkey) - const router = useRouter() - const [draft, setDraft] = useState(() => buildInitialDraft(existingPresentation, existingAuthorName)) - const [validationError, setValidationError] = useState(null) - const [deleting, setDeleting] = useState(false) - - // Update authorName when profile changes - useEffect(() => { - syncAuthorNameIntoDraft({ existingAuthorName, draftAuthorName: draft.authorName, hasExistingPresentation: Boolean(existingPresentation), setDraft }) - }, [existingAuthorName, existingPresentation, draft.authorName]) - - const handleSubmit = useCallback( - async (e: FormEvent) => { - e.preventDefault() - await submitPresentationDraft({ draft, setValidationError, publishPresentation }) - }, - [draft, publishPresentation] - ) - - const handleDelete = useCallback(async () => { - await deletePresentationFlow({ - existingPresentationId: existingPresentation?.id, - deletePresentation, - router, - setDeleting, - setValidationError, - }) - }, [existingPresentation, deletePresentation, router]) - - return { loading, error, success, draft, setDraft, validationError, handleSubmit, deleting, handleDelete } -} - -function buildInitialDraft(existingPresentation: Article | null | undefined, existingAuthorName: string | undefined): AuthorPresentationDraft { - if (existingPresentation) { - const { presentation, contentDescription } = extractPresentationData(existingPresentation) - const authorName = existingPresentation.title.replace(/^Présentation de /, '') ?? existingAuthorName ?? '' - return { - authorName, - presentation, - contentDescription, - mainnetAddress: existingPresentation.mainnetAddress ?? '', - ...(existingPresentation.bannerUrl ? { pictureUrl: existingPresentation.bannerUrl } : {}), - } - } - return { - authorName: existingAuthorName ?? '', - presentation: '', - contentDescription: '', - mainnetAddress: '', - } -} - -function syncAuthorNameIntoDraft(params: { - existingAuthorName: string | undefined - draftAuthorName: string - hasExistingPresentation: boolean - setDraft: (updater: (prev: AuthorPresentationDraft) => AuthorPresentationDraft) => void -}): void { - if (!params.existingAuthorName || params.hasExistingPresentation || params.existingAuthorName === params.draftAuthorName) { - return - } - params.setDraft((prev) => ({ ...prev, authorName: params.existingAuthorName as string })) -} - -async function submitPresentationDraft(params: { - draft: AuthorPresentationDraft - setValidationError: (value: string | null) => void - publishPresentation: (draft: AuthorPresentationDraft) => Promise -}): Promise { - const error = validatePresentationDraft(params.draft) - if (error) { - params.setValidationError(error) - return - } - params.setValidationError(null) - await params.publishPresentation(params.draft) -} - -function validatePresentationDraft(draft: AuthorPresentationDraft): string | null { - const address = draft.mainnetAddress.trim() - if (!ADDRESS_PATTERN.test(address)) { - return t('presentation.validation.invalidAddress') - } - if (!draft.authorName.trim()) { - return t('presentation.validation.authorNameRequired') - } - return null -} - -async function deletePresentationFlow(params: { - existingPresentationId: string | undefined - deletePresentation: (articleId: string) => Promise - router: ReturnType - setDeleting: (value: boolean) => void - setValidationError: (value: string | null) => void -}): Promise { - if (!params.existingPresentationId) { - return - } - const confirmed = await userConfirm(t('presentation.delete.confirm')) - if (!confirmed) { - return - } - params.setDeleting(true) - params.setValidationError(null) - try { - await params.deletePresentation(params.existingPresentationId) - await params.router.push('/') - } catch (e) { - params.setValidationError(e instanceof Error ? e.message : t('presentation.delete.error')) - } finally { - params.setDeleting(false) - } -} - -function NoAccountActionButtons({ - onGenerate, - onImport, -}: { - onGenerate: () => void - onImport: () => void -}): React.ReactElement { - return ( -
- - -
- ) -} - -function NoAccountView(): React.ReactElement { - const [showImportModal, setShowImportModal] = useState(false) - const [showRecoveryStep, setShowRecoveryStep] = useState(false) - const [showUnlockModal, setShowUnlockModal] = useState(false) - const [recoveryPhrase, setRecoveryPhrase] = useState([]) - const [npub, setNpub] = useState('') - const [generating, setGenerating] = useState(false) - const [error, setError] = useState(null) - - const handleGenerate = (): Promise => generateNoAccount({ setGenerating, setError, setRecoveryPhrase, setNpub, setShowRecoveryStep }) - const handleRecoveryContinue = (): void => transitionToUnlock({ setShowRecoveryStep, setShowUnlockModal }) - const handleUnlockSuccess = (): void => resetNoAccountAfterUnlock({ setShowUnlockModal, setRecoveryPhrase, setNpub }) - const handleImportSuccess = (): void => { - setShowImportModal(false) - setShowUnlockModal(true) - } - - return ( - { void handleGenerate() }} - onImport={() => setShowImportModal(true)} - modals={ - setShowImportModal(false)} - onImportSuccess={handleImportSuccess} - showRecoveryStep={showRecoveryStep} - recoveryPhrase={recoveryPhrase} - npub={npub} - onRecoveryContinue={handleRecoveryContinue} - showUnlockModal={showUnlockModal} - onUnlockSuccess={handleUnlockSuccess} - onCloseUnlock={() => setShowUnlockModal(false)} - /> - } - /> - ) -} - -async function generateNoAccount(params: { - setGenerating: (value: boolean) => void - setError: (value: string | null) => void - setRecoveryPhrase: (value: string[]) => void - setNpub: (value: string) => void - setShowRecoveryStep: (value: boolean) => void -}): Promise { - params.setGenerating(true) - params.setError(null) - try { - const { nostrAuthService } = await import('@/lib/nostrAuth') - const result = await nostrAuthService.createAccount() - params.setRecoveryPhrase(result.recoveryPhrase) - params.setNpub(result.npub) - params.setShowRecoveryStep(true) - } catch (e) { - params.setError(e instanceof Error ? e.message : t('account.create.error.failed')) - } finally { - params.setGenerating(false) - } -} - -function transitionToUnlock(params: { setShowRecoveryStep: (value: boolean) => void; setShowUnlockModal: (value: boolean) => void }): void { - params.setShowRecoveryStep(false) - params.setShowUnlockModal(true) -} - -function resetNoAccountAfterUnlock(params: { - setShowUnlockModal: (value: boolean) => void - setRecoveryPhrase: (value: string[]) => void - setNpub: (value: string) => void -}): void { - params.setShowUnlockModal(false) - params.setRecoveryPhrase([]) - params.setNpub('') -} - -function NoAccountCard(params: { - error: string | null - generating: boolean - onGenerate: () => void - onImport: () => void - modals: React.ReactElement -}): React.ReactElement { - return ( -
-
-

Créez un compte ou importez votre clé secrète pour commencer

- {params.error &&

{params.error}

} - - {params.generating &&

Génération du compte...

} - {params.modals} -
-
- ) -} - -function NoAccountModals(params: { - showImportModal: boolean - onImportSuccess: () => void - onCloseImport: () => void - showRecoveryStep: boolean - recoveryPhrase: string[] - npub: string - onRecoveryContinue: () => void - showUnlockModal: boolean - onUnlockSuccess: () => void - onCloseUnlock: () => void -}): React.ReactElement { - return ( - <> - {params.showImportModal && } - {params.showRecoveryStep && } - {params.showUnlockModal && } - - ) -} - -function AuthorPresentationFormView(props: { - pubkey: string | null - profile: { name?: string; pubkey: string } | null -}): React.ReactElement { - const { checkPresentationExists } = useAuthorPresentation(props.pubkey) - const presentation = useExistingPresentation({ pubkey: props.pubkey, checkPresentationExists }) - const state = useAuthorPresentationState(props.pubkey, props.profile?.name, presentation.existingPresentation) - - if (!props.pubkey) { - return - } - if (presentation.loadingPresentation) { - return - } - if (state.success) { - return - } - return ( - { void state.handleDelete() }} - hasExistingPresentation={presentation.existingPresentation !== null} - /> - ) -} - -function LoadingNotice(): React.ReactElement { - return ( -
-

{t('common.loading')}

-
- ) -} - -function useExistingPresentation(params: { - pubkey: string | null - checkPresentationExists: () => Promise
-}): { existingPresentation: Article | null; loadingPresentation: boolean } { - const [existingPresentation, setExistingPresentation] = useState
(null) - const [loadingPresentation, setLoadingPresentation] = useState(true) - const { pubkey, checkPresentationExists } = params - - useEffect(() => { - void loadExistingPresentation({ pubkey, checkPresentationExists, setExistingPresentation, setLoadingPresentation }) - }, [pubkey, checkPresentationExists]) - - return { existingPresentation, loadingPresentation } -} - -async function loadExistingPresentation(params: { - pubkey: string | null - checkPresentationExists: () => Promise
- setExistingPresentation: (value: Article | null) => void - setLoadingPresentation: (value: boolean) => void -}): Promise { - if (!params.pubkey) { - params.setLoadingPresentation(false) - return - } - try { - params.setExistingPresentation(await params.checkPresentationExists()) - } catch (e) { - console.error('Error loading presentation:', e) - } finally { - params.setLoadingPresentation(false) - } -} - -function useAutoLoadPubkey(accountExists: boolean | null, pubkey: string | null, connect: () => Promise): void { - useEffect(() => { - if (accountExists === true && !pubkey) { - void connect() - } - }, [accountExists, pubkey, connect]) -} - -export function AuthorPresentationEditor(): React.ReactElement { - const { pubkey, profile, accountExists, connect } = useNostrAuth() - useAutoLoadPubkey(accountExists, pubkey ?? null, connect) - return -} +export { AuthorPresentationEditor } from './authorPresentationEditor/AuthorPresentationEditor' diff --git a/components/KeyManagementManager.tsx b/components/KeyManagementManager.tsx index e2b6c6a..ed62ef0 100644 --- a/components/KeyManagementManager.tsx +++ b/components/KeyManagementManager.tsx @@ -1,559 +1 @@ -import { useState, useEffect } from 'react' -import { nostrAuthService } from '@/lib/nostrAuth' -import { keyManagementService } from '@/lib/keyManagement' -import { nip19 } from 'nostr-tools' -import { t } from '@/lib/i18n' -import { SyncProgressBar } from './SyncProgressBar' - -interface PublicKeys { - publicKey: string - npub: string -} - -export function KeyManagementManager(): React.ReactElement { - const [publicKeys, setPublicKeys] = useState(null) - const [accountExists, setAccountExists] = useState(false) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [importKey, setImportKey] = useState('') - const [importing, setImporting] = useState(false) - const [showImportForm, setShowImportForm] = useState(false) - const [showReplaceWarning, setShowReplaceWarning] = useState(false) - const [recoveryPhrase, setRecoveryPhrase] = useState(null) - const [newNpub, setNewNpub] = useState(null) - const [copiedNpub, setCopiedNpub] = useState(false) - const [copiedPublicKey, setCopiedPublicKey] = useState(false) - const [copiedRecoveryPhrase, setCopiedRecoveryPhrase] = useState(false) - - useEffect(() => { - void loadKeys() - }, []) - - async function loadKeys(): Promise { - try { - setLoading(true) - setError(null) - const exists = await nostrAuthService.accountExists() - setAccountExists(exists) - - if (exists) { - const keys = await keyManagementService.getPublicKeys() - if (keys) { - setPublicKeys(keys) - } - } - } catch (e) { - const errorMessage = e instanceof Error ? e.message : t('settings.keyManagement.loading') - setError(errorMessage) - console.error('Error loading keys:', e) - } finally { - setLoading(false) - } - } - - function extractKeyFromUrl(url: string): string | null { - try { - // Try to parse as URL - const urlObj = new URL(url) - // Check if it's a nostr:// URL with nsec - if (urlObj.protocol === 'nostr:' || urlObj.protocol === 'nostr://') { - const path = urlObj.pathname ?? urlObj.href.replace(/^nostr:?\/\//, '') - if (path.startsWith('nsec')) { - return path - } - } - // Check if URL contains nsec - const nsecMatch = url.match(/nsec1[a-z0-9]+/i) - if (nsecMatch) { - return nsecMatch[0] - } - return null - } catch { - return extractKeyFromText(url) - } - } - - function extractKeyFromText(text: string): string { - const nsec = extractNsec(text) - if (nsec) { - return nsec - } - return text.trim() - } - - function extractNsec(text: string): string | null { - const nsecMatch = text.match(/nsec1[a-z0-9]+/i) - return nsecMatch?.[0] ?? null - } - - function isValidPrivateKeyFormat(key: string): boolean { - try { - const decoded = nip19.decode(key) - if (decoded.type !== 'nsec') { - return false - } - return typeof decoded.data === 'string' || decoded.data instanceof Uint8Array - } catch { - return /^[0-9a-f]{64}$/i.test(key) - } - } - - async function handleImport(): Promise { - if (!importKey.trim()) { - setError(t('settings.keyManagement.import.error.required')) - return - } - - // Extract key from URL or text - const extractedKey = extractKeyFromUrl(importKey.trim()) - if (!extractedKey) { - setError(t('settings.keyManagement.import.error.invalid')) - return - } - - // Validate key format - if (!isValidPrivateKeyFormat(extractedKey)) { - setError(t('settings.keyManagement.import.error.invalid')) - return - } - - // If account exists, show warning - if (accountExists) { - setShowReplaceWarning(true) - return - } - - await performImport(extractedKey) - } - - async function performImport(key: string): Promise { - try { - setImporting(true) - setError(null) - setShowReplaceWarning(false) - - // If account exists, delete it first - if (accountExists) { - await nostrAuthService.deleteAccount() - } - - // Create new account with imported key - const result = await nostrAuthService.createAccount(key) - setRecoveryPhrase(result.recoveryPhrase) - setNewNpub(result.npub) - setImportKey('') - setShowImportForm(false) - await loadKeys() - - // Sync user content via Service Worker - if (result.publicKey) { - const { swClient } = await import('@/lib/swClient') - const isReady = await swClient.isReady() - if (isReady) { - void swClient.startUserSync(result.publicKey) - } - } - } catch (e) { - const errorMessage = e instanceof Error ? e.message : t('settings.keyManagement.import.error.failed') - setError(errorMessage) - console.error('Error importing key:', e) - } finally { - setImporting(false) - } - } - - async function handleCopyRecoveryPhrase(): Promise { - if (!recoveryPhrase) { - return - } - try { - await navigator.clipboard.writeText(recoveryPhrase.join(' ')) - setCopiedRecoveryPhrase(true) - setTimeout(() => { - setCopiedRecoveryPhrase(false) - }, 2000) - } catch (e) { - console.error('Error copying recovery phrase:', e) - } - } - - async function handleCopyNpub(): Promise { - if (!publicKeys?.npub) { - return - } - try { - await navigator.clipboard.writeText(publicKeys.npub) - setCopiedNpub(true) - setTimeout(() => { - setCopiedNpub(false) - }, 2000) - } catch (e) { - console.error('Error copying npub:', e) - } - } - - async function handleCopyPublicKey(): Promise { - if (!publicKeys?.publicKey) { - return - } - try { - await navigator.clipboard.writeText(publicKeys.publicKey) - setCopiedPublicKey(true) - setTimeout(() => { - setCopiedPublicKey(false) - }, 2000) - } catch (e) { - console.error('Error copying public key:', e) - } - } - - if (loading) { - return ( -
-

{t('settings.keyManagement.loading')}

-
- ) - } - - return ( -
-
-

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

- - - - - - {/* Sync Progress Bar - Always show if connected, even if publicKeys not loaded yet */} - - - - - { - setShowImportForm(true) - setError(null) - }} - /> - - { - setImportKey(value) - setError(null) - }} - onCancel={() => { - setShowImportForm(false) - setImportKey('') - setError(null) - }} - onImport={() => { - void handleImport() - }} - onDismissReplaceWarning={() => { - setShowReplaceWarning(false) - }} - onConfirmReplace={() => { - void performImport(extractKeyFromUrl(importKey.trim()) ?? importKey.trim()) - }} - /> - - {/* Recovery Phrase Display (after import) */} - { - setRecoveryPhrase(null) - setNewNpub(null) - void loadKeys() - }} - /> -
-
- ) -} - -function KeyManagementErrorBanner(params: { error: string | null }): React.ReactElement | null { - if (!params.error) { - return null - } - return ( -
-

{params.error}

-
- ) -} - -function KeyManagementPublicKeysPanel(params: { - publicKeys: PublicKeys | null - copiedNpub: boolean - copiedPublicKey: boolean - onCopyNpub: () => Promise - onCopyPublicKey: () => Promise -}): React.ReactElement | null { - if (!params.publicKeys) { - return null - } - return ( -
- - -
- ) -} - -function KeyManagementKeyCard(params: { - label: string - value: string - copied: boolean - onCopy: () => Promise -}): React.ReactElement { - return ( -
-
-

{params.label}

- -
-

{params.value}

-
- ) -} - -function KeyManagementNoAccountBanner(params: { - publicKeys: PublicKeys | null - accountExists: boolean -}): React.ReactElement | null { - if (params.publicKeys || params.accountExists) { - return null - } - return ( -
-

{t('settings.keyManagement.noAccount.title')}

-

{t('settings.keyManagement.noAccount.description')}

-
- ) -} - -function KeyManagementImportButton(params: { - accountExists: boolean - showImportForm: boolean - onClick: () => void -}): React.ReactElement | null { - if (params.showImportForm) { - return null - } - return ( - - ) -} - -function KeyManagementImportForm(params: { - accountExists: boolean - showImportForm: boolean - showReplaceWarning: boolean - importing: boolean - importKey: string - onChangeImportKey: (value: string) => void - onCancel: () => void - onImport: () => void - onDismissReplaceWarning: () => void - onConfirmReplace: () => void -}): React.ReactElement | null { - if (!params.showImportForm) { - return null - } - return ( -
-
-

{t('settings.keyManagement.import.warning.title')}

-

- {params.accountExists && ( -

- )} -

- -
- -