From 602e23c719a0d583b194d627761efff14fb9b074 Mon Sep 17 00:00:00 2001 From: Stixx Date: Wed, 27 Nov 2024 17:00:59 +0100 Subject: [PATCH 01/39] fix: clicking on marked fileds is losing input text/squiggle, squiggle field is mobile friendly --- src/components/MarkFormField/style.module.scss | 2 +- src/components/PDFView/PdfMarking.tsx | 4 ++-- src/utils/const.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MarkFormField/style.module.scss b/src/components/MarkFormField/style.module.scss index 0125140..686595f 100644 --- a/src/components/MarkFormField/style.module.scss +++ b/src/components/MarkFormField/style.module.scss @@ -122,7 +122,7 @@ align-items: center; grid-gap: 15px; box-shadow: 0 -2px 4px 0 rgb(0, 0, 0, 0.1); - max-width: 750px; + max-width: 450px; &.expanded { display: flex; diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index e11ee65..222c393 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -70,8 +70,8 @@ const PdfMarking = (props: PdfMarkingProps) => { const handleMarkClick = (id: number) => { const nextMark = currentUserMarks.find((mark) => mark.mark.id === id) - setSelectedMark(nextMark!) - setSelectedMarkValue(nextMark?.mark.value ?? EMPTY) + + if (nextMark) handleCurrentUserMarkChange(nextMark) } const handleCurrentUserMarkChange = (mark: CurrentUserMark) => { diff --git a/src/utils/const.ts b/src/utils/const.ts index bf38404..522f242 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -119,6 +119,6 @@ export const SIGNATURE_PAD_OPTIONS = { } as const export const SIGNATURE_PAD_SIZE = { - width: 600, + width: 300, height: 300 } From 413da78c5c06b5b35740692f6b7c00125a7c8969 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 29 Nov 2024 11:15:10 +0100 Subject: [PATCH 02/39] fix(mark): css position --- src/components/DrawPDFFields/style.module.scss | 16 ++++++++++++---- src/components/PDFView/PdfMarkItem.tsx | 2 +- src/pages/verify/style.module.scss | 4 ---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 0a89fbf..7ae0de3 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -38,10 +38,6 @@ visibility: hidden; } - &.edited { - outline: 1px dotted #01aaad; - } - .resizeHandle { position: absolute; right: -5px; @@ -99,3 +95,15 @@ height: 21px; } } + +.signingRectangle { + position: absolute; + outline: 1px solid #01aaad; + z-index: 40; + background-color: #01aaad4b; + cursor: pointer; + + &.edited { + outline: 1px dotted #01aaad; + } +} diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx index 0eaa49d..a1dddb3 100644 --- a/src/components/PDFView/PdfMarkItem.tsx +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -32,7 +32,7 @@ const PdfMarkItem = forwardRef(
Date: Fri, 29 Nov 2024 12:17:23 +0100 Subject: [PATCH 03/39] fix(create): remove small drawn fields Closes #234 --- src/components/DrawPDFFields/index.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 09c7d8f..687b7e3 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -22,6 +22,11 @@ import { AvatarIconButton } from '../UserAvatarIconButton' import { UserAvatar } from '../UserAvatar' import _ from 'lodash' +const MINIMUM_RECT_SIZE = { + width: 21, + height: 21 +} as const + const DEFAULT_START_SIZE = { width: 140, height: 40 @@ -91,6 +96,7 @@ export const DrawPDFFields = (props: Props) => { window.removeEventListener('pointerup', handlePointerUp) window.removeEventListener('pointercancel', handlePointerUp) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const refreshPdfFiles = () => { @@ -149,6 +155,18 @@ export const DrawPDFFields = (props: Props) => { * @param event Pointer event */ const handlePointerUp = () => { + sigitFiles.forEach((s) => { + s.pages?.forEach((p) => { + // Remove small rects (accidentaly created) + p.drawnFields = p.drawnFields.filter( + (f) => + !( + f.width < MINIMUM_RECT_SIZE.width || + f.height < MINIMUM_RECT_SIZE.height + ) + ) + }) + }) setMouseState((prev) => { return { ...prev, @@ -157,6 +175,7 @@ export const DrawPDFFields = (props: Props) => { resizing: false } }) + refreshPdfFiles() } /** From 6193c20ac30e849c0e6f36b18c0f4c529e31bdd0 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 29 Nov 2024 12:45:37 +0100 Subject: [PATCH 04/39] refactor(create): update comment --- src/components/DrawPDFFields/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 687b7e3..b9fd162 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -157,7 +157,7 @@ export const DrawPDFFields = (props: Props) => { const handlePointerUp = () => { sigitFiles.forEach((s) => { s.pages?.forEach((p) => { - // Remove small rects (accidentaly created) + // Remove drawn fields below the MINIMUM_RECT_SIZE threshhold p.drawnFields = p.drawnFields.filter( (f) => !( From 8a9910db87dcd27488ef1588a18e4480b16adde8 Mon Sep 17 00:00:00 2001 From: Stixx Date: Mon, 2 Dec 2024 11:51:02 +0100 Subject: [PATCH 05/39] fix: include purplepage and userkindpages relays when searching for user in create page --- package-lock.json | 8 ++++---- package.json | 2 +- src/pages/create/index.tsx | 13 +++++++++++-- src/utils/relays.ts | 4 ++-- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index a03e759..0b038f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "jszip": "3.10.1", "lodash": "4.17.21", "mui-file-input": "4.0.4", - "nostr-login": "^1.6.6", + "nostr-login": "file:../../nostrdev-com/nostr-login/packages/auth/dist/nostr-login-1.6.12.tgz", "nostr-tools": "2.7.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.4.168", @@ -6415,9 +6415,9 @@ } }, "node_modules/nostr-login": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/nostr-login/-/nostr-login-1.6.6.tgz", - "integrity": "sha512-XOpB9nG3Qgt7iea7gA1zn4TaTfUKCKGdCHKwErqLPtMk/q1Rhkzj5cq/66iU0WqC6mSiwENfTy1p4qaM7HzMtg==", + "version": "1.6.12", + "resolved": "file:../../nostrdev-com/nostr-login/packages/auth/dist/nostr-login-1.6.12.tgz", + "integrity": "sha512-7qqhWSrA3Hr/An2+s7JIr1HhIpOsdwfNRgKBctPLrfjIQbiMYQd8/S25Pvv20s09yA/tS8zPgWYUUdeKoPDDNg==", "license": "MIT", "dependencies": { "@nostr-dev-kit/ndk": "^2.3.1", diff --git a/package.json b/package.json index ab49da0..2357f80 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "jszip": "3.10.1", "lodash": "4.17.21", "mui-file-input": "4.0.4", - "nostr-login": "^1.6.6", + "nostr-login": "file:../../nostrdev-com/nostr-login/packages/auth/dist/nostr-login-1.6.12.tgz", "nostr-tools": "2.7.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.4.168", diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index a65a559..0ad59ea 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -52,7 +52,8 @@ import { updateUsersAppData, uploadToFileStorage, DEFAULT_TOOLBOX, - settleAllFullfilfedPromises + settleAllFullfilfedPromises, + DEFAULT_LOOK_UP_RELAY_LIST } from '../../utils' import { Container } from '../../components/Container' import fileListStyles from '../../components/FileList/style.module.scss' @@ -159,6 +160,14 @@ export const CreatePage = () => { const metadataController = MetadataController.getInstance() const relaySet = await metadataController.findRelayListMetadata(usersPubkey) + + DEFAULT_LOOK_UP_RELAY_LIST.forEach((relay) => { + if (!relaySet.write.includes(relay)) relaySet.write.push(relay) + if (!relaySet.read.includes(relay)) relaySet.read.push(relay) + }) + + const uniqueReadRelaySet = [...new Set(relaySet.read)] + const searchTerm = searchString.trim() relayController @@ -167,7 +176,7 @@ export const CreatePage = () => { kinds: [0], search: searchTerm }, - [...relaySet.write] + uniqueReadRelaySet ) .then((events) => { console.log('events', events) diff --git a/src/utils/relays.ts b/src/utils/relays.ts index bfef7aa..7a0ad56 100644 --- a/src/utils/relays.ts +++ b/src/utils/relays.ts @@ -80,8 +80,8 @@ const getUserRelaySet = (tags: string[][]): RelaySet => { } const getDefaultRelaySet = (): RelaySet => ({ - read: [SIGIT_RELAY], - write: [SIGIT_RELAY] + read: DEFAULT_LOOK_UP_RELAY_LIST, + write: DEFAULT_LOOK_UP_RELAY_LIST }) const getDefaultRelayMap = (): RelayMap => ({ From 834d70d7747148d08b3c19720aca8d56b3d0de68 Mon Sep 17 00:00:00 2001 From: Stixx Date: Mon, 2 Dec 2024 15:01:29 +0100 Subject: [PATCH 06/39] fix: better UX when clicking on logo when on home screen or `home` button in footer --- src/components/AppBar/AppBar.tsx | 12 +++++++++++- src/components/Footer/Footer.tsx | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 6a5862d..a4f653e 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -121,7 +121,17 @@ export const AppBar = () => { - Logo navigate('/')} /> + Logo { + if (window.location.pathname === '/') { + location.reload() + } else { + navigate('/') + } + }} + /> diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index ead7cec..249b8c4 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -68,6 +68,12 @@ export const Footer = () => }} component={Link} to={'/'} + onClick={(event) => { + if (window.location.pathname === '/') { + event.preventDefault() + window.scrollTo(0, 0) + } + }} variant={'text'} > Home From 24463a53c50f00313e3502438ae5f47254635253 Mon Sep 17 00:00:00 2001 From: Stixx Date: Mon, 2 Dec 2024 15:34:17 +0100 Subject: [PATCH 07/39] fix: inform user then search term provided no results --- src/pages/create/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index a65a559..588957b 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -199,8 +199,11 @@ export const CreatePage = () => { return uniqueEvents }, []) - console.log('fineFilteredEvents', fineFilteredEvents) + console.info('fineFilteredEvents', fineFilteredEvents) setFoundUsers(fineFilteredEvents) + + if (!fineFilteredEvents.length) + toast.info('No user found with the provided search term') }) .catch((error) => { console.error(error) From bbe34b60116af3081a45ec55dde6e6e7620818a3 Mon Sep 17 00:00:00 2001 From: Stixx Date: Tue, 3 Dec 2024 10:48:40 +0100 Subject: [PATCH 08/39] fix: display `no results` when no submissions are found --- src/pages/home/index.tsx | 72 +++++++++++++++++++------------- src/pages/home/style.module.scss | 7 ++++ 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 1a29021..f7166e2 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -135,6 +135,46 @@ export const HomePage = () => { const [filter, setFilter] = useState('Show all') const [sort, setSort] = useState('desc') + const renderSubmissions = () => { + const submissions = Object.keys(parsedSigits) + .filter((s) => { + const { title, signedStatus } = parsedSigits[s] + const isMatch = title?.toLowerCase().includes(q.toLowerCase()) + switch (filter) { + case 'Completed': + return signedStatus === SigitStatus.Complete && isMatch + case 'In-progress': + return signedStatus === SigitStatus.Partial && isMatch + case 'Show all': + return isMatch + default: + console.error('Filter case not handled.') + } + }) + .sort((a, b) => { + const x = parsedSigits[a].createdAt ?? 0 + const y = parsedSigits[b].createdAt ?? 0 + return sort === 'desc' ? y - x : x - y + }) + + if (submissions.length) { + return submissions.map((key) => ( + + )) + } else { + return ( +
+

No results

+
+ ) + } + } + return (
@@ -233,36 +273,8 @@ export const HomePage = () => { )} -
- {Object.keys(parsedSigits) - .filter((s) => { - const { title, signedStatus } = parsedSigits[s] - const isMatch = title?.toLowerCase().includes(q.toLowerCase()) - switch (filter) { - case 'Completed': - return signedStatus === SigitStatus.Complete && isMatch - case 'In-progress': - return signedStatus === SigitStatus.Partial && isMatch - case 'Show all': - return isMatch - default: - console.error('Filter case not handled.') - } - }) - .sort((a, b) => { - const x = parsedSigits[a].createdAt ?? 0 - const y = parsedSigits[b].createdAt ?? 0 - return sort === 'desc' ? y - x : x - y - }) - .map((key) => ( - - ))} -
+ +
{renderSubmissions()}
diff --git a/src/pages/home/style.module.scss b/src/pages/home/style.module.scss index 63917a0..5efd4fd 100644 --- a/src/pages/home/style.module.scss +++ b/src/pages/home/style.module.scss @@ -99,3 +99,10 @@ gap: 25px; grid-template-columns: repeat(auto-fit, minmax(365px, 1fr)); } + +.noResults { + display: flex; + justify-content: center; + font-weight: normal; + color: #a1a1a1; +} From 98fbe80648e6b8e80af1e6c15de8bb963d27c070 Mon Sep 17 00:00:00 2001 From: Stixx Date: Tue, 3 Dec 2024 11:25:31 +0100 Subject: [PATCH 09/39] fix: create page, improving message "preparing document for signing" --- src/pages/create/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index a65a559..d9aeb03 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -761,7 +761,7 @@ export const CreatePage = () => { title } - setLoadingSpinnerDesc('Signing nostr event for create signature') + setLoadingSpinnerDesc('Preparing document(s) for signing') const createSignature = await signEventForMetaFile( JSON.stringify(content), From 0a0a9bef348e798d37d892b602e19e82e41d0fba Mon Sep 17 00:00:00 2001 From: Stixx Date: Fri, 6 Dec 2024 13:24:10 +0100 Subject: [PATCH 10/39] feat: Sign Directly From the Marking Screen fix: Marking inputs glitches, losing values --- package-lock.json | 27 ++ package.json | 1 + src/components/FileList/index.tsx | 43 +- src/components/MarkFormField/index.tsx | 97 ++++- .../MarkFormField/style.module.scss | 4 + src/components/PDFView/PdfMarking.tsx | 28 +- src/pages/sign/index.tsx | 389 ++++++------------ src/pages/verify/index.tsx | 123 +++++- src/utils/const.ts | 2 +- 9 files changed, 405 insertions(+), 309 deletions(-) diff --git a/package-lock.json b/package-lock.json index a03e759..ebeec88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "idb": "8.0.0", "jszip": "3.10.1", "lodash": "4.17.21", + "material-ui-popup-state": "^5.3.1", "mui-file-input": "4.0.4", "nostr-login": "^1.6.6", "nostr-tools": "2.7.0", @@ -3330,6 +3331,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -5976,6 +5983,26 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/material-ui-popup-state": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-5.3.1.tgz", + "integrity": "sha512-mmx1DsQwF/2cmcpHvS/QkUwOQG2oAM+cDEQU0DaZVYnvwKyTB3AFgu8l1/E+LQFausmzpSJoljwQSZXkNvt7eA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.6", + "@types/prop-types": "^15.7.3", + "@types/react": "^18.0.26", + "classnames": "^2.2.6", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@mui/material": "^5.0.0 || ^6.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", diff --git a/package.json b/package.json index ab49da0..562aadf 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "idb": "8.0.0", "jszip": "3.10.1", "lodash": "4.17.21", + "material-ui-popup-state": "^5.3.1", "mui-file-input": "4.0.4", "nostr-login": "^1.6.6", "nostr-tools": "2.7.0", diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx index a47ace7..51ccdce 100644 --- a/src/components/FileList/index.tsx +++ b/src/components/FileList/index.tsx @@ -1,14 +1,17 @@ import { CurrentUserFile } from '../../types/file.ts' import styles from './style.module.scss' -import { Button } from '@mui/material' +import { Button, Menu, MenuItem } from '@mui/material' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCheck } from '@fortawesome/free-solid-svg-icons' +import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state' +import React from 'react' interface FileListProps { files: CurrentUserFile[] currentFile: CurrentUserFile setCurrentFile: (file: CurrentUserFile) => void - handleDownload: () => void + handleExport: () => void + handleEncryptedExport?: () => void downloadLabel?: string } @@ -16,7 +19,8 @@ const FileList = ({ files, currentFile, setCurrentFile, - handleDownload, + handleExport, + handleEncryptedExport, downloadLabel }: FileListProps) => { const isActive = (file: CurrentUserFile) => file.id === currentFile.id @@ -42,9 +46,36 @@ const FileList = ({ ))} - + + {!downloadLabel && ( + + {(popupState) => ( + + + + { + popupState.close + handleExport() + }} + > + Export Files + + { + popupState.close + handleEncryptedExport && handleEncryptedExport() + }} + > + Export Encrypted Files + + + + )} + + )}
) } diff --git a/src/components/MarkFormField/index.tsx b/src/components/MarkFormField/index.tsx index 18bbf31..718a119 100644 --- a/src/components/MarkFormField/index.tsx +++ b/src/components/MarkFormField/index.tsx @@ -8,12 +8,14 @@ import { } from '../../utils' import React, { useState } from 'react' import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheck } from '@fortawesome/free-solid-svg-icons' interface MarkFormFieldProps { currentUserMarks: CurrentUserMark[] handleCurrentUserMarkChange: (mark: CurrentUserMark) => void handleSelectedMarkValueChange: (value: string) => void - handleSubmit: (event: React.FormEvent) => void + handleSubmit: (event: React.MouseEvent) => void selectedMark: CurrentUserMark selectedMarkValue: string } @@ -30,11 +32,13 @@ const MarkFormField = ({ handleCurrentUserMarkChange }: MarkFormFieldProps) => { const [displayActions, setDisplayActions] = useState(true) + const [complete, setComplete] = useState(false) + const isReadyToSign = () => isCurrentUserMarksComplete(currentUserMarks) || isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue) const isCurrent = (currentMark: CurrentUserMark) => - currentMark.id === selectedMark.id + currentMark.id === selectedMark.id && !complete const isDone = (currentMark: CurrentUserMark) => isCurrent(currentMark) ? !!selectedMarkValue : currentMark.isCompleted const findNext = () => { @@ -46,13 +50,36 @@ const MarkFormField = ({ const handleFormSubmit = (event: React.FormEvent) => { event.preventDefault() console.log('handle form submit runs...') - return isReadyToSign() - ? handleSubmit(event) - : handleCurrentUserMarkChange(findNext()!) + + // Without this line, we lose mark values when switching + handleCurrentUserMarkChange(selectedMark) + + if (!complete) { + isReadyToSign() + ? setComplete(true) + : handleCurrentUserMarkChange(findNext()!) + } } + const toggleActions = () => setDisplayActions(!displayActions) const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type) + const handleCurrentUserMarkClick = (mark: CurrentUserMark) => { + setComplete(false) + handleCurrentUserMarkChange(mark) + } + + const handleSelectCompleteMark = () => { + handleCurrentUserMarkChange(selectedMark) + setComplete(true) + } + + const handleSignAndComplete = ( + event: React.MouseEvent + ) => { + handleSubmit(event) + } + return (
@@ -78,25 +105,43 @@ const MarkFormField = ({
-

Add {markLabel}

+ {!complete && ( +

Add {markLabel}

+ )} + {complete &&

Finish

}
-
handleFormSubmit(e)}> - + {!complete && ( + handleFormSubmit(e)}> + +
+ +
+ + )} + + {complete && (
-
- + )} +
{currentUserMarks.map((mark, index) => { @@ -104,7 +149,7 @@ const MarkFormField = ({
@@ -114,6 +159,20 @@ const MarkFormField = ({
) })} +
+ + {complete && ( +
+ )} +
diff --git a/src/components/MarkFormField/style.module.scss b/src/components/MarkFormField/style.module.scss index 686595f..3825146 100644 --- a/src/components/MarkFormField/style.module.scss +++ b/src/components/MarkFormField/style.module.scss @@ -216,3 +216,7 @@ flex-direction: column; grid-gap: 5px; } + +.finishPage { + padding: 1px 0; +} diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index 222c393..f0d4f60 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -24,11 +24,12 @@ import { interface PdfMarkingProps { currentUserMarks: CurrentUserMark[] files: CurrentUserFile[] - handleDownload: () => void + handleExport: () => void + handleEncryptedExport: () => void + handleSign: () => void meta: Meta | null otherUserMarks: Mark[] setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void - setIsMarksCompleted: (isMarksCompleted: boolean) => void setUpdatedMarks: (markToUpdate: Mark) => void } @@ -42,10 +43,11 @@ const PdfMarking = (props: PdfMarkingProps) => { const { files, currentUserMarks, - setIsMarksCompleted, setCurrentUserMarks, setUpdatedMarks, - handleDownload, + handleExport, + handleEncryptedExport, + handleSign, meta, otherUserMarks } = props @@ -86,11 +88,18 @@ const PdfMarking = (props: PdfMarkingProps) => { updatedSelectedMark ) setCurrentUserMarks(updatedCurrentUserMarks) - setSelectedMarkValue(mark.currentValue ?? EMPTY) - setSelectedMark(mark) + + // If clicking on the same mark, don't update the value, otherwise do update + if (mark.id !== selectedMark.id) { + setSelectedMarkValue(mark.currentValue ?? EMPTY) + setSelectedMark(mark) + } } - const handleSubmit = (event: React.FormEvent) => { + /** + * Sign and Complete + */ + const handleSubmit = (event: React.MouseEvent) => { event.preventDefault() if (!selectedMarkValue || !selectedMark) return @@ -106,8 +115,8 @@ const PdfMarking = (props: PdfMarkingProps) => { ) setCurrentUserMarks(updatedCurrentUserMarks) setSelectedMark(null) - setIsMarksCompleted(true) setUpdatedMarks(updatedMark.mark) + handleSign() } // const updateCurrentUserMarkValues = () => { @@ -132,7 +141,8 @@ const PdfMarking = (props: PdfMarkingProps) => { files={files} currentFile={currentFile} setCurrentFile={setCurrentFile} - handleDownload={handleDownload} + handleExport={handleExport} + handleEncryptedExport={handleEncryptedExport} /> )}
diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index f30ecdd..7636e61 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -1,17 +1,15 @@ -import { Box, Button, Typography } from '@mui/material' import axios from 'axios' import saveAs from 'file-saver' import JSZip from 'jszip' import _ from 'lodash' -import { MuiFileInput } from 'mui-file-input' import { Event, verifyEvent } from 'nostr-tools' import { useCallback, useEffect, useState } from 'react' -import { useAppSelector } from '../../hooks/store' +import { useAppSelector } from '../../hooks' import { useLocation, useNavigate, useParams } from 'react-router-dom' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { NostrController } from '../../controllers' -import { appPublicRoutes } from '../../routes' +import { appPrivateRoutes, appPublicRoutes } from '../../routes' import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types' import { decryptArrayBuffer, @@ -36,15 +34,10 @@ import { timeout, processMarks } from '../../utils' -import { Container } from '../../components/Container' -import { DisplayMeta } from './internal/displayMeta' -import styles from './style.module.scss' import { CurrentUserMark, Mark } from '../../types/mark.ts' -import { getLastSignersSig, isFullySigned } from '../../utils/sign.ts' import { filterMarksByPubkey, getCurrentUserMarks, - isCurrentUserMarksComplete, updateMarks } from '../../utils' import PdfMarking from '../../components/PDFView/PdfMarking.tsx' @@ -53,16 +46,10 @@ import { getZipWithFiles, SigitFile } from '../../utils/file.ts' -import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts' +import { ARRAY_BUFFER, DEFLATE } from '../../utils' import { generateTimestamp } from '../../utils/opentimestamps.ts' import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx' -enum SignedStatus { - Fully_Signed, - User_Is_Next_Signer, - User_Is_Not_Next_Signer -} - export const SignPage = () => { const navigate = useNavigate() const location = useLocation() @@ -100,17 +87,12 @@ export const SignPage = () => { } } - const [displayInput, setDisplayInput] = useState(false) - - const [selectedFile, setSelectedFile] = useState(null) - const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({}) const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [meta, setMeta] = useState(null) - const [signedStatus, setSignedStatus] = useState() const [submittedBy, setSubmittedBy] = useState() @@ -124,66 +106,14 @@ export const SignPage = () => { [key: string]: string | null }>({}) - const [signedBy, setSignedBy] = useState<`npub1${string}`[]>([]) - - const [nextSinger, setNextSinger] = useState() - - // This state variable indicates whether the logged-in user is a signer, a creator, or neither. - const [isSignerOrCreator, setIsSignerOrCreator] = useState(false) - const usersPubkey = useAppSelector((state) => state.auth.usersPubkey) const nostrController = NostrController.getInstance() const [currentUserMarks, setCurrentUserMarks] = useState( [] ) - const [isMarksCompleted, setIsMarksCompleted] = useState(false) const [otherUserMarks, setOtherUserMarks] = useState([]) - useEffect(() => { - if (signers.length > 0) { - // check if all signers have signed then its fully signed - if (isFullySigned(signers, signedBy)) { - setSignedStatus(SignedStatus.Fully_Signed) - } else { - for (const signer of signers) { - if (!signedBy.includes(signer)) { - // signers in meta.json are in npub1 format - // so, convert it to hex before setting to nextSigner - setNextSinger(npubToHex(signer)!) - - const usersNpub = hexToNpub(usersPubkey!) - - if (signer === usersNpub) { - // logged in user is the next signer - setSignedStatus(SignedStatus.User_Is_Next_Signer) - } else { - setSignedStatus(SignedStatus.User_Is_Not_Next_Signer) - } - - break - } - } - } - } else { - // there's no signer just viewers. So its fully signed - setSignedStatus(SignedStatus.Fully_Signed) - } - - // Determine and set the status of the user - if (submittedBy && usersPubkey && submittedBy === usersPubkey) { - // If the submission was made by the user, set the status to true - setIsSignerOrCreator(true) - } else if (usersPubkey) { - // Convert the user's public key from hex to npub format - const usersNpub = hexToNpub(usersPubkey) - if (signers.includes(usersNpub)) { - // If the user's npub is in the list of signers, set the status to true - setIsSignerOrCreator(true) - } - } - }, [signers, signedBy, usersPubkey, submittedBy]) - useEffect(() => { const handleUpdatedMeta = async (meta: Meta) => { const createSignatureEvent = await parseJson( @@ -278,10 +208,7 @@ export const SignPage = () => { setOtherUserMarks(otherUserMarks) setCurrentUserMarks(currentUserMarks) - setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks)) } - - setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[]) } if (meta) { @@ -290,29 +217,6 @@ export const SignPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [meta, usersPubkey]) - const handleDownload = async () => { - if (Object.entries(files).length === 0 || !meta || !usersPubkey) return - setLoadingSpinnerDesc('Generating file') - try { - const zip = await getZipWithFiles(meta, files) - const arrayBuffer = await zip.generateAsync({ - type: ARRAY_BUFFER, - compression: DEFLATE, - compressionOptions: { - level: 6 - } - }) - if (!arrayBuffer) return - const blob = new Blob([arrayBuffer]) - saveAs(blob, `exported-${unixNow()}.sigit.zip`) - } catch (error) { - console.log('error in zip:>> ', error) - if (error instanceof Error) { - toast.error(error.message || 'Error occurred in generating zip file') - } - } - } - const decrypt = useCallback( async (file: File) => { setLoadingSpinnerDesc('Decrypting file') @@ -424,7 +328,6 @@ export const SignPage = () => { }) } else { setIsLoading(false) - setDisplayInput(true) } }, [decryptedArrayBuffer, uploadedZip, metaInNavState, decrypt]) @@ -541,9 +444,6 @@ export const SignPage = () => { setFiles(files) setCurrentFileHashes(fileHashes) - - setDisplayInput(false) - setLoadingSpinnerDesc('Parsing meta.json') const metaFileContent = await readContentOfZipEntry( @@ -571,21 +471,6 @@ export const SignPage = () => { setMeta(parsedMetaJson) } - const handleDecrypt = async () => { - if (!selectedFile) return - - setIsLoading(true) - const arrayBuffer = await decrypt(selectedFile) - - if (!arrayBuffer) { - setIsLoading(false) - toast.error('Error decrypting file') - return - } - - handleDecryptedArrayBuffer(arrayBuffer) - } - const handleSign = async () => { if (Object.entries(files).length === 0 || !meta) return @@ -640,6 +525,13 @@ export const SignPage = () => { setMeta(updatedMeta) setIsLoading(false) } + + if (metaInNavState) { + const createSignature = JSON.parse(metaInNavState.createSignature) + navigate(`${appPublicRoutes.verify}/${createSignature.id}`) + } else { + navigate(appPrivateRoutes.homePage) + } } // Sign the event for the meta file @@ -730,6 +622,14 @@ export const SignPage = () => { }) } + // Check if the current user is the last signer + const checkIsLastSigner = (signers: string[]): boolean => { + const usersNpub = hexToNpub(usersPubkey!) + const lastSignerIndex = signers.length - 1 + const signerIndex = signers.indexOf(usersNpub) + return signerIndex === lastSignerIndex + } + // Handle errors during zip file generation const handleZipError = (err: unknown) => { console.log('Error in zip:>> ', err) @@ -740,7 +640,7 @@ export const SignPage = () => { return null } - // Handle the online flow: update users app data and send notifications + // Handle the online flow: update users' app data and send notifications const handleOnlineFlow = async (meta: Meta) => { setLoadingSpinnerDesc('Updating users app data') const updatedEvent = await updateUsersAppData(meta) @@ -795,82 +695,102 @@ export const SignPage = () => { setIsLoading(false) } - // Check if the current user is the last signer - const checkIsLastSigner = (signers: string[]): boolean => { - const usersNpub = hexToNpub(usersPubkey!) - const lastSignerIndex = signers.length - 1 - const signerIndex = signers.indexOf(usersNpub) - return signerIndex === lastSignerIndex - } - const handleExport = async () => { if (Object.entries(files).length === 0 || !meta || !usersPubkey) return - - const usersNpub = hexToNpub(usersPubkey) - if ( - !signers.includes(usersNpub) && - !viewers.includes(usersNpub) && - submittedBy !== usersNpub - ) - return - - setIsLoading(true) - setLoadingSpinnerDesc('Signing nostr event') - - if (!meta) return - - const prevSig = getLastSignersSig(meta, signers) - if (!prevSig) return - - const signedEvent = await signEventForMetaFile( - JSON.stringify({ - prevSig - }), - nostrController, - setIsLoading - ) - - if (!signedEvent) return - - const exportSignature = JSON.stringify(signedEvent, null, 2) - - const stringifiedMeta = JSON.stringify( - { - ...meta, - exportSignature - }, - null, - 2 - ) - - const zip = await getZipWithFiles(meta, files) - zip.file('meta.json', stringifiedMeta) - - const arrayBuffer = await zip - .generateAsync({ - type: 'arraybuffer', - compression: 'DEFLATE', + setLoadingSpinnerDesc('Generating file') + try { + const zip = await getZipWithFiles(meta, files) + const arrayBuffer = await zip.generateAsync({ + type: ARRAY_BUFFER, + compression: DEFLATE, compressionOptions: { level: 6 } }) - .catch((err) => { - console.log('err in zip:>> ', err) - setIsLoading(false) - toast.error(err.message || 'Error occurred in generating zip file') - return null - }) - - if (!arrayBuffer) return - - const blob = new Blob([arrayBuffer]) - saveAs(blob, `exported-${unixNow()}.sigit.zip`) - - setIsLoading(false) - - navigate(appPublicRoutes.verify) + if (!arrayBuffer) return + const blob = new Blob([arrayBuffer]) + saveAs(blob, `exported-${unixNow()}.sigit.zip`) + } catch (error) { + console.log('error in zip:>> ', error) + if (error instanceof Error) { + toast.error(error.message || 'Error occurred in generating zip file') + } + } } + /** + * @Depricated + * It's the same as {@link handleExport} but the code is not the same + * leaving it for the reference + */ + // const handleExport = async () => { + // if (Object.entries(files).length === 0 || !meta || !usersPubkey) return + // + // const usersNpub = hexToNpub(usersPubkey) + // if ( + // !signers.includes(usersNpub) && + // !viewers.includes(usersNpub) && + // submittedBy !== usersNpub + // ) + // return + // + // setIsLoading(true) + // setLoadingSpinnerDesc('Signing nostr event') + // + // if (!meta) return + // + // const prevSig = getLastSignersSig(meta, signers) + // if (!prevSig) return + // + // const signedEvent = await signEventForMetaFile( + // JSON.stringify({ + // prevSig + // }), + // nostrController, + // setIsLoading + // ) + // + // if (!signedEvent) return + // + // const exportSignature = JSON.stringify(signedEvent, null, 2) + // + // const stringifiedMeta = JSON.stringify( + // { + // ...meta, + // exportSignature + // }, + // null, + // 2 + // ) + // + // const zip = await getZipWithFiles(meta, files) + // zip.file('meta.json', stringifiedMeta) + // + // const arrayBuffer = await zip + // .generateAsync({ + // type: 'arraybuffer', + // compression: 'DEFLATE', + // compressionOptions: { + // level: 6 + // } + // }) + // .catch((err) => { + // console.log('err in zip:>> ', err) + // setIsLoading(false) + // toast.error(err.message || 'Error occurred in generating zip file') + // return null + // }) + // + // if (!arrayBuffer) return + // + // const blob = new Blob([arrayBuffer]) + // saveAs(blob, `exported-${unixNow()}.sigit.zip`) + // + // setIsLoading(false) + // + // navigate(appPublicRoutes.verify) + // } + const handleEncryptedExport = async () => { if (Object.entries(files).length === 0 || !meta) return @@ -944,90 +864,17 @@ export const SignPage = () => { return } - if (!isMarksCompleted && signedStatus === SignedStatus.User_Is_Next_Signer) { - return ( - - ) - } - return ( - <> - - {displayInput && ( - <> - - Select sigit file - - - - setSelectedFile(value)} - /> - - - {selectedFile && ( - - - - )} - - )} - - {submittedBy && Object.entries(files).length > 0 && meta && ( - <> - - - {signedStatus === SignedStatus.Fully_Signed && ( - - - - )} - - {signedStatus === SignedStatus.User_Is_Next_Signer && ( - - - - )} - - {isSignerOrCreator && ( - - - - )} - - )} - - + ) } diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 515a257..1d91fa4 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -23,7 +23,10 @@ import { getCurrentUserFiles, updateUsersAppData, npubToHex, - sendNotification + sendNotification, + generateEncryptionKey, + encryptArrayBuffer, + generateKeysFile } from '../../utils' import styles from './style.module.scss' import { useLocation, useParams } from 'react-router-dom' @@ -597,6 +600,120 @@ export const VerifyPage = () => { setIsLoading(false) } + // Handle errors during zip file generation + const handleZipError = (err: unknown) => { + console.log('Error in zip:>> ', err) + setIsLoading(false) + if (err instanceof Error) { + toast.error(err.message || 'Error occurred in generating zip file') + } + return null + } + + // Check if the current user is the last signer + const checkIsLastSigner = (signers: string[]): boolean => { + const usersNpub = hexToNpub(usersPubkey!) + const lastSignerIndex = signers.length - 1 + const signerIndex = signers.indexOf(usersNpub) + return signerIndex === lastSignerIndex + } + + // create final zip file + const createFinalZipFile = async ( + encryptedArrayBuffer: ArrayBuffer, + encryptionKey: string + ): Promise => { + // Get the current timestamp in seconds + const blob = new Blob([encryptedArrayBuffer]) + // Create a File object with the Blob data + const file = new File([blob], `compressed.sigit`, { + type: 'application/sigit' + }) + + const isLastSigner = checkIsLastSigner(signers) + + const userSet = new Set() + + if (isLastSigner) { + if (submittedBy) { + userSet.add(submittedBy) + } + + signers.forEach((signer) => { + userSet.add(npubToHex(signer)!) + }) + + viewers.forEach((viewer) => { + userSet.add(npubToHex(viewer)!) + }) + } else { + const usersNpub = hexToNpub(usersPubkey!) + const signerIndex = signers.indexOf(usersNpub) + const nextSigner = signers[signerIndex + 1] + userSet.add(npubToHex(nextSigner)!) + } + + const keysFileContent = await generateKeysFile( + Array.from(userSet), + encryptionKey + ) + if (!keysFileContent) return null + + const zip = new JSZip() + zip.file(`compressed.sigit`, file) + zip.file('keys.json', keysFileContent) + + const arraybuffer = await zip + .generateAsync({ + type: 'arraybuffer', + compression: 'DEFLATE', + compressionOptions: { level: 6 } + }) + .catch(handleZipError) + + if (!arraybuffer) return null + + return new File([new Blob([arraybuffer])], `${unixNow()}.sigit.zip`, { + type: 'application/zip' + }) + } + + const handleEncryptedExport = async () => { + if (Object.entries(files).length === 0 || !meta) return + + const stringifiedMeta = JSON.stringify(meta, null, 2) + const zip = await getZipWithFiles(meta, files) + + zip.file('meta.json', stringifiedMeta) + + const arrayBuffer = await zip + .generateAsync({ + type: 'arraybuffer', + compression: 'DEFLATE', + compressionOptions: { + level: 6 + } + }) + .catch((err) => { + console.log('err in zip:>> ', err) + setIsLoading(false) + toast.error(err.message || 'Error occurred in generating zip file') + return null + }) + + if (!arrayBuffer) return + + const key = await generateEncryptionKey() + + setLoadingSpinnerDesc('Encrypting zip file') + const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key) + + const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key) + + if (!finalZipFile) return + saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`) + } + return ( <> {isLoading && } @@ -640,8 +757,8 @@ export const VerifyPage = () => { )} currentFile={currentFile} setCurrentFile={setCurrentFile} - handleDownload={handleMarkedExport} - downloadLabel="Download Sigit" + handleExport={handleMarkedExport} + handleEncryptedExport={handleEncryptedExport} /> ) } diff --git a/src/utils/const.ts b/src/utils/const.ts index 522f242..2b8e822 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -120,5 +120,5 @@ export const SIGNATURE_PAD_OPTIONS = { export const SIGNATURE_PAD_SIZE = { width: 300, - height: 300 + height: 150 } From d0231b0652c8db805d1e9b84dfbf22528419612a Mon Sep 17 00:00:00 2001 From: Stixx Date: Fri, 6 Dec 2024 14:41:44 +0100 Subject: [PATCH 11/39] chore: typo --- src/pages/sign/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 7636e61..ab95318 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -719,7 +719,7 @@ export const SignPage = () => { } /** - * @Depricated + * @deprecated * It's the same as {@link handleExport} but the code is not the same * leaving it for the reference */ From 69efd9e09d043403c987a5ab7fddd68158a1d22a Mon Sep 17 00:00:00 2001 From: Stixx Date: Fri, 6 Dec 2024 15:49:57 +0100 Subject: [PATCH 12/39] fix: clicking logo not redirecting to home --- src/components/AppBar/AppBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index a4f653e..e7f5d95 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -125,7 +125,7 @@ export const AppBar = () => { src="/logo.svg" alt="Logo" onClick={() => { - if (window.location.pathname === '/') { + if (['', '#/'].includes(window.location.hash)) { location.reload() } else { navigate('/') From 0fd0f26fc7ef87aea7c48ccddae822d23d2b8853 Mon Sep 17 00:00:00 2001 From: Stixx Date: Fri, 6 Dec 2024 16:22:23 +0100 Subject: [PATCH 13/39] fix: counterpart search NIP05 glitching --- src/pages/create/index.tsx | 41 ++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 212f7bf..dfe3338 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -148,6 +148,17 @@ export const CreatePage = () => { [setUserInput] ) + const handleSearchUserNip05 = async ( + nip05: string + ): Promise => { + const { pubkey } = await queryNip05(nip05).catch((err) => { + console.error(err) + return { pubkey: null } + }) + + return pubkey + } + const handleSearchUsers = async (searchValue?: string) => { const searchString = searchValue || userSearchInput || undefined @@ -224,7 +235,9 @@ export const CreatePage = () => { }) }, [foundUsers]) - const handleInputKeyDown = (event: React.KeyboardEvent) => { + const handleInputKeyDown = async ( + event: React.KeyboardEvent + ) => { if ( event.code === KeyboardCode.Enter || event.code === KeyboardCode.NumpadEnter @@ -238,7 +251,18 @@ export const CreatePage = () => { } else { // Otherwize if search already provided some results, user must manually click the search button if (!foundUsers.length) { - handleSearchUsers() + // If it's NIP05 send request to .well-known + if (userSearchInput.includes('@')) { + const pubkey = await handleSearchUserNip05(userSearchInput) + + if (pubkey) { + setUserInput(userSearchInput) + } else { + toast.error(`No user found with the NIP05: ${userSearchInput}`) + } + } else { + handleSearchUsers() + } } } } @@ -959,19 +983,6 @@ export const CreatePage = () => { } else { disarmAddOnEnter() } - } else if (value.includes('@')) { - // Seems like it's nip05 format - const { pubkey } = await queryNip05(value).catch((err) => { - console.error(err) - return { pubkey: null } - }) - - if (pubkey) { - // Arm the manual user npub add after enter is hit, we don't want to trigger search - setPastedUserNpubOrNip05(hexToNpub(pubkey)) - } else { - disarmAddOnEnter() - } } else { // Disarm the add user on enter hit, and trigger search after 1 second disarmAddOnEnter() From 3d1bdece4d881f347e974506af9d01d9be01f4f7 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 6 Dec 2024 20:00:38 +0100 Subject: [PATCH 14/39] feat(meta): send notifications with blossom instead of meta.json --- src/hooks/useSigitMeta.tsx | 8 +-- src/pages/create/index.tsx | 16 +++-- src/pages/sign/index.tsx | 14 +++-- src/pages/verify/index.tsx | 13 ++++- src/types/core.ts | 9 +++ src/types/errors/MetaStorageError.ts | 26 +++++++++ src/utils/meta.ts | 87 +++++++++++++++++++++++++++- src/utils/nostr.ts | 66 ++++++++++++++++----- 8 files changed, 210 insertions(+), 29 deletions(-) create mode 100644 src/types/errors/MetaStorageError.ts diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index 85841f2..5c1159e 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -45,7 +45,7 @@ export interface FlatMeta isValid: boolean // Decryption - encryptionKey: string | null + encryptionKey: string | undefined // Parsed Document Signatures parsedSignatureEvents: { @@ -101,7 +101,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { [signer: `npub1${string}`]: SignStatus }>({}) - const [encryptionKey, setEncryptionKey] = useState(null) + const [encryptionKey, setEncryptionKey] = useState() useEffect(() => { if (!meta) return @@ -143,7 +143,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { setMarkConfig(markConfig) setZipUrl(zipUrl) - let encryptionKey: string | null = null + let encryptionKey: string | undefined if (meta.keys) { const { sender, keys } = meta.keys // Retrieve the user's public key from the state @@ -161,7 +161,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { 'An error occurred in decrypting encryption key', err ) - return null + return undefined }) encryptionKey = decrypted diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 212f7bf..f0c1f0c 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -31,6 +31,7 @@ import { KeyboardCode, Meta, ProfileMetadata, + SigitNotification, SignedEvent, User, UserRole @@ -52,7 +53,8 @@ import { updateUsersAppData, uploadToFileStorage, DEFAULT_TOOLBOX, - settleAllFullfilfedPromises + settleAllFullfilfedPromises, + uploadMetaToFileStorage } from '../../utils' import { Container } from '../../components/Container' import fileListStyles from '../../components/FileList/style.module.scss' @@ -782,7 +784,7 @@ export const CreatePage = () => { } // Send notifications to signers and viewers - const sendNotifications = (meta: Meta) => { + const sendNotifications = (notification: SigitNotification) => { // no need to send notification to self so remove it from the list const receivers = ( signers.length > 0 @@ -790,7 +792,7 @@ export const CreatePage = () => { : viewers.map((viewer) => viewer.pubkey) ).filter((receiver) => receiver !== usersPubkey) - return receivers.map((receiver) => sendNotification(receiver, meta)) + return receivers.map((receiver) => sendNotification(receiver, notification)) } const extractNostrId = (stringifiedEvent: string): string => { @@ -865,11 +867,17 @@ export const CreatePage = () => { } setLoadingSpinnerDesc('Updating user app data') + + const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey) + const event = await updateUsersAppData(meta) if (!event) return setLoadingSpinnerDesc('Sending notifications to counterparties') - const promises = sendNotifications(meta) + const promises = sendNotifications({ + metaUrl, + keys: meta.keys + }) await Promise.all(promises) .then(() => { diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index f30ecdd..346e226 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -34,7 +34,8 @@ import { updateUsersAppData, findOtherUserMarks, timeout, - processMarks + processMarks, + uploadMetaToFileStorage } from '../../utils' import { Container } from '../../components/Container' import { DisplayMeta } from './internal/displayMeta' @@ -635,7 +636,7 @@ export const SignPage = () => { } if (await isOnline()) { - await handleOnlineFlow(updatedMeta) + await handleOnlineFlow(updatedMeta, encryptionKey) } else { setMeta(updatedMeta) setIsLoading(false) @@ -741,7 +742,10 @@ export const SignPage = () => { } // Handle the online flow: update users app data and send notifications - const handleOnlineFlow = async (meta: Meta) => { + const handleOnlineFlow = async ( + meta: Meta, + encryptionKey: string | undefined + ) => { setLoadingSpinnerDesc('Updating users app data') const updatedEvent = await updateUsersAppData(meta) if (!updatedEvent) { @@ -749,6 +753,8 @@ export const SignPage = () => { return } + const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey) + const userSet = new Set<`npub1${string}`>() if (submittedBy && submittedBy !== usersPubkey) { userSet.add(hexToNpub(submittedBy)) @@ -781,7 +787,7 @@ export const SignPage = () => { setLoadingSpinnerDesc('Sending notifications') const users = Array.from(userSet) const promises = users.map((user) => - sendNotification(npubToHex(user)!, meta) + sendNotification(npubToHex(user)!, { metaUrl, keys: meta.keys }) ) await Promise.all(promises) .then(() => { diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 515a257..e870a23 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -23,7 +23,8 @@ import { getCurrentUserFiles, updateUsersAppData, npubToHex, - sendNotification + sendNotification, + uploadMetaToFileStorage } from '../../utils' import styles from './style.module.scss' import { useLocation, useParams } from 'react-router-dom' @@ -351,6 +352,11 @@ export const VerifyPage = () => { const updatedEvent = await updateUsersAppData(updatedMeta) if (!updatedEvent) return + const metaUrl = await uploadMetaToFileStorage( + updatedMeta, + encryptionKey + ) + const userSet = new Set<`npub1${string}`>() signers.forEach((signer) => { if (signer !== usersPubkey) { @@ -364,7 +370,10 @@ export const VerifyPage = () => { const users = Array.from(userSet) const promises = users.map((user) => - sendNotification(npubToHex(user)!, updatedMeta) + sendNotification(npubToHex(user)!, { + metaUrl, + keys: meta.keys! + }) ) await Promise.all(promises) diff --git a/src/types/core.ts b/src/types/core.ts index df55a07..f07dbf7 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -83,3 +83,12 @@ export interface UserAppData { export interface DocSignatureEvent extends Event { parsedContent?: SignedEventContent } + +export interface SigitNotification { + metaUrl: string + keys?: { sender: string; keys: { [user: `npub1${string}`]: string } } +} + +export function isSigitNotification(obj: unknown): obj is SigitNotification { + return typeof (obj as SigitNotification).metaUrl === 'string' +} diff --git a/src/types/errors/MetaStorageError.ts b/src/types/errors/MetaStorageError.ts new file mode 100644 index 0000000..a5bc2cd --- /dev/null +++ b/src/types/errors/MetaStorageError.ts @@ -0,0 +1,26 @@ +import { Jsonable } from '.' + +export enum MetaStorageErrorType { + 'ENCRYPTION_KEY_REQUIRED' = 'Encryption key is required.', + 'HASHING_FAILED' = "Can't get encrypted file hash.", + 'FETCH_FAILED' = 'Fetching meta.json requires an encryption key.', + 'HASH_VERIFICATION_FAILED' = 'Unable to verify meta.json.', + 'DECRYPTION_FAILED' = 'Error decryping meta.json.', + 'UNHANDLED_FETCH_ERROR' = 'Unable to fetch meta.json. Something went wrong.' +} + +export class MetaStorageError extends Error { + public readonly context?: Jsonable + + constructor( + message: MetaStorageErrorType, + options: { cause?: Error; context?: Jsonable } = {} + ) { + const { cause, context } = options + + super(message, { cause }) + this.name = this.constructor.name + + this.context = context + } +} diff --git a/src/utils/meta.ts b/src/utils/meta.ts index c915f66..8052abf 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -1,5 +1,12 @@ import { CreateSignatureEventContent, Meta } from '../types' -import { fromUnixTimestamp, parseJson } from '.' +import { + decryptArrayBuffer, + encryptArrayBuffer, + fromUnixTimestamp, + getHash, + parseJson, + uploadToFileStorage +} from '.' import { Event, verifyEvent } from 'nostr-tools' import { toast } from 'react-toastify' import { extractFileExtensions } from './file' @@ -8,6 +15,11 @@ import { MetaParseError, MetaParseErrorType } from '../types/errors/MetaParseError' +import axios from 'axios' +import { + MetaStorageError, + MetaStorageErrorType +} from '../types/errors/MetaStorageError' export enum SignStatus { Signed = 'Signed', @@ -126,3 +138,76 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { } } } + +export const uploadMetaToFileStorage = async ( + meta: Meta, + encryptionKey: string | undefined +) => { + // Value is the stringified meta object + const value = JSON.stringify(meta) + const encoder = new TextEncoder() + + // Encode it to the arrayBuffer + const uint8Array = encoder.encode(value) + + if (!encryptionKey) { + throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED) + } + + // Encrypt the file contents with the same encryption key from the create signature + const encryptedArrayBuffer = await encryptArrayBuffer( + uint8Array, + encryptionKey + ) + + const hash = await getHash(encryptedArrayBuffer) + if (!hash) { + throw new MetaStorageError(MetaStorageErrorType.HASHING_FAILED) + } + + // Create the encrypted json file from array buffer and hash + const file = new File([encryptedArrayBuffer], `${hash}.json`) + const url = await uploadToFileStorage(file) + + return url +} + +export const fetchMetaFromFileStorage = async ( + url: string, + encryptionKey: string | undefined +) => { + if (!encryptionKey) { + throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED) + } + + const encryptedArrayBuffer = await axios.get(url, { + responseType: 'arraybuffer' + }) + + // Verify hash + const parts = url.split('/') + const urlHash = parts[parts.length - 1] + const hash = await getHash(encryptedArrayBuffer.data) + if (hash !== urlHash) { + throw new MetaStorageError(MetaStorageErrorType.HASH_VERIFICATION_FAILED) + } + + const arrayBuffer = await decryptArrayBuffer( + encryptedArrayBuffer.data, + encryptionKey + ).catch((err) => { + throw new MetaStorageError(MetaStorageErrorType.DECRYPTION_FAILED, { + cause: err + }) + }) + + if (arrayBuffer) { + // Decode meta.json and parse + const decoder = new TextDecoder() + const json = decoder.decode(arrayBuffer) + const meta = await parseJson(json) + return meta + } + + throw new MetaStorageError(MetaStorageErrorType.UNHANDLED_FETCH_ERROR) +} diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index ec8c97e..ed51fac 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -29,12 +29,20 @@ import { } from '../store/actions' import { Keys } from '../store/auth/types' import store from '../store/store' -import { Meta, ProfileMetadata, SignedEvent, UserAppData } from '../types' +import { + isSigitNotification, + Meta, + ProfileMetadata, + SigitNotification, + SignedEvent, + UserAppData +} from '../types' import { getDefaultRelayMap } from './relays' import { parseJson, removeLeadingSlash } from './string' import { timeout } from './utils' import { getHash } from './hash' import { SIGIT_BLOSSOM } from './const.ts' +import { fetchMetaFromFileStorage } from './meta.ts' /** * Generates a `d` tag for userAppData @@ -908,17 +916,44 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => { if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938) return - const meta = await parseJson(internalUnsignedEvent.content).catch( - (err) => { - console.log( - 'An error occurred in parsing the internal unsigned event', - err - ) - return null - } - ) + const parsedContent = await parseJson( + internalUnsignedEvent.content + ).catch((err) => { + console.log('An error occurred in parsing the internal unsigned event', err) + return null + }) - if (!meta) return + if (!parsedContent) return + let meta: Meta + if (isSigitNotification(parsedContent)) { + const notification = parsedContent + let encryptionKey: string | undefined + if (!notification.keys) return + + const { sender, keys } = notification.keys + + // Retrieve the user's public key from the state + const usersPubkey = store.getState().auth.usersPubkey! + const usersNpub = hexToNpub(usersPubkey) + + // Check if the user's public key is in the keys object + if (usersNpub in keys) { + // Instantiate the NostrController to decrypt the encryption key + const nostrController = NostrController.getInstance() + const decrypted = await nostrController + .nip04Decrypt(sender, keys[usersNpub]) + .catch((err) => { + console.log('An error occurred in decrypting encryption key', err) + return undefined + }) + + encryptionKey = decrypted + } + + meta = await fetchMetaFromFileStorage(notification.metaUrl, encryptionKey) + } else { + meta = parsedContent + } await updateUsersAppData(meta) } @@ -926,9 +961,12 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => { /** * Function to send a notification to a specified receiver. * @param receiver - The recipient's public key. - * @param meta - Metadata associated with the notification. + * @param notification - Url pointing to metadata associated with the notification on blossom and keys to decrypt. */ -export const sendNotification = async (receiver: string, meta: Meta) => { +export const sendNotification = async ( + receiver: string, + notification: SigitNotification +) => { // Retrieve the user's public key from the state const usersPubkey = store.getState().auth.usersPubkey! @@ -936,7 +974,7 @@ export const sendNotification = async (receiver: string, meta: Meta) => { const unsignedEvent: UnsignedEvent = { kind: 938, pubkey: usersPubkey, - content: JSON.stringify(meta), + content: JSON.stringify(notification), tags: [], created_at: unixNow() } From 7b29d7055eeb6bf7f9bbcc06fb2ecf0962157046 Mon Sep 17 00:00:00 2001 From: Stixx Date: Fri, 6 Dec 2024 20:58:20 +0100 Subject: [PATCH 15/39] fix: search counterparts nip05 does not need to include '@' --- src/pages/create/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index dfe3338..1defde7 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -251,10 +251,15 @@ export const CreatePage = () => { } else { // Otherwize if search already provided some results, user must manually click the search button if (!foundUsers.length) { - // If it's NIP05 send request to .well-known - if (userSearchInput.includes('@')) { + // If it's NIP05 (includes @ or is a valid domain) send request to .well-known + const domainRegex = /^[a-zA-Z0-9@.-]+\.[a-zA-Z]{2,}$/ + if (domainRegex.test(userSearchInput)) { + setSearchUsersLoading(true) + const pubkey = await handleSearchUserNip05(userSearchInput) + setSearchUsersLoading(false) + if (pubkey) { setUserInput(userSearchInput) } else { From afbe05b4c88d5042c17f4a59c0739dc8942642a9 Mon Sep 17 00:00:00 2001 From: Stixx Date: Fri, 6 Dec 2024 21:00:52 +0100 Subject: [PATCH 16/39] fix: footer 'Home' button scroll to top when on home page, fixed logic --- src/components/Footer/Footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index 249b8c4..c7459e3 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -69,7 +69,7 @@ export const Footer = () => component={Link} to={'/'} onClick={(event) => { - if (window.location.pathname === '/') { + if (['', '#/'].includes(window.location.hash)) { event.preventDefault() window.scrollTo(0, 0) } From 555504f42f030028af6b280d664e1024d63e1e12 Mon Sep 17 00:00:00 2001 From: Stixx Date: Mon, 9 Dec 2024 09:44:22 +0100 Subject: [PATCH 17/39] fix: nostr-login custom outbox relays --- src/layouts/Main.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index 19ac4d9..3e6be26 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -134,7 +134,15 @@ export const MainLayout = () => { initNostrLogin({ methods: ['connect', 'extension', 'local'], noBanner: true, - onAuth: handleNostrAuth + onAuth: handleNostrAuth, + outboxRelays: [ + 'wss://purplepag.es', + 'wss://relay.nos.social', + 'wss://user.kindpag.es', + 'wss://relay.damus.io', + 'wss://nos.lol', + 'wss://relay.sigit.io' + ] }).catch((error) => { console.error('Failed to initialize Nostr-Login', error) }) From 8d8c38e90bce13c3a0b833a5598a790c2086a267 Mon Sep 17 00:00:00 2001 From: Stixx Date: Mon, 9 Dec 2024 22:37:27 +0100 Subject: [PATCH 18/39] fix: addressing comments --- src/components/FileList/index.tsx | 61 +++++++++++++++---------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx index 51ccdce..a073ca4 100644 --- a/src/components/FileList/index.tsx +++ b/src/components/FileList/index.tsx @@ -12,7 +12,6 @@ interface FileListProps { setCurrentFile: (file: CurrentUserFile) => void handleExport: () => void handleEncryptedExport?: () => void - downloadLabel?: string } const FileList = ({ @@ -20,8 +19,7 @@ const FileList = ({ currentFile, setCurrentFile, handleExport, - handleEncryptedExport, - downloadLabel + handleEncryptedExport }: FileListProps) => { const isActive = (file: CurrentUserFile) => file.id === currentFile.id return ( @@ -47,35 +45,34 @@ const FileList = ({ ))} - {!downloadLabel && ( - - {(popupState) => ( - - - - { - popupState.close - handleExport() - }} - > - Export Files - - { - popupState.close - handleEncryptedExport && handleEncryptedExport() - }} - > - Export Encrypted Files - - - - )} - - )} + + {(popupState) => ( + + + + { + popupState.close + handleExport() + }} + > + Export Files + + { + popupState.close + typeof handleEncryptedExport === 'function' && + handleEncryptedExport() + }} + > + Export Encrypted Files + + + + )} +
) } From 6716c3da636f9c0a1b168cb2cb0a98da97a6c5c3 Mon Sep 17 00:00:00 2001 From: Stixx Date: Tue, 10 Dec 2024 14:42:12 +0100 Subject: [PATCH 19/39] fix: including signatures in both export and encrypted export --- src/pages/sign/index.tsx | 197 +++++++++++++++---------------------- src/pages/verify/index.tsx | 151 +++++++++++++--------------- 2 files changed, 151 insertions(+), 197 deletions(-) diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index ab95318..16fa3c4 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -32,7 +32,8 @@ import { updateUsersAppData, findOtherUserMarks, timeout, - processMarks + processMarks, + ARRAY_BUFFER } from '../../utils' import { CurrentUserMark, Mark } from '../../types/mark.ts' import { @@ -46,9 +47,10 @@ import { getZipWithFiles, SigitFile } from '../../utils/file.ts' -import { ARRAY_BUFFER, DEFLATE } from '../../utils' +import { DEFLATE } from '../../utils' import { generateTimestamp } from '../../utils/opentimestamps.ts' import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx' +import { getLastSignersSig } from '../../utils/sign.ts' export const SignPage = () => { const navigate = useNavigate() @@ -696,124 +698,19 @@ export const SignPage = () => { } const handleExport = async () => { - if (Object.entries(files).length === 0 || !meta || !usersPubkey) return - setLoadingSpinnerDesc('Generating file') - try { - const zip = await getZipWithFiles(meta, files) - const arrayBuffer = await zip.generateAsync({ - type: ARRAY_BUFFER, - compression: DEFLATE, - compressionOptions: { - level: 6 - } - }) - if (!arrayBuffer) return - const blob = new Blob([arrayBuffer]) - saveAs(blob, `exported-${unixNow()}.sigit.zip`) - } catch (error) { - console.log('error in zip:>> ', error) - if (error instanceof Error) { - toast.error(error.message || 'Error occurred in generating zip file') - } - } + const arrayBuffer = await prepareZipExport() + if (!arrayBuffer) return + + const blob = new Blob([arrayBuffer]) + saveAs(blob, `exported-${unixNow()}.sigit.zip`) + + setIsLoading(false) + + navigate(appPublicRoutes.verify) } - /** - * @deprecated - * It's the same as {@link handleExport} but the code is not the same - * leaving it for the reference - */ - // const handleExport = async () => { - // if (Object.entries(files).length === 0 || !meta || !usersPubkey) return - // - // const usersNpub = hexToNpub(usersPubkey) - // if ( - // !signers.includes(usersNpub) && - // !viewers.includes(usersNpub) && - // submittedBy !== usersNpub - // ) - // return - // - // setIsLoading(true) - // setLoadingSpinnerDesc('Signing nostr event') - // - // if (!meta) return - // - // const prevSig = getLastSignersSig(meta, signers) - // if (!prevSig) return - // - // const signedEvent = await signEventForMetaFile( - // JSON.stringify({ - // prevSig - // }), - // nostrController, - // setIsLoading - // ) - // - // if (!signedEvent) return - // - // const exportSignature = JSON.stringify(signedEvent, null, 2) - // - // const stringifiedMeta = JSON.stringify( - // { - // ...meta, - // exportSignature - // }, - // null, - // 2 - // ) - // - // const zip = await getZipWithFiles(meta, files) - // zip.file('meta.json', stringifiedMeta) - // - // const arrayBuffer = await zip - // .generateAsync({ - // type: 'arraybuffer', - // compression: 'DEFLATE', - // compressionOptions: { - // level: 6 - // } - // }) - // .catch((err) => { - // console.log('err in zip:>> ', err) - // setIsLoading(false) - // toast.error(err.message || 'Error occurred in generating zip file') - // return null - // }) - // - // if (!arrayBuffer) return - // - // const blob = new Blob([arrayBuffer]) - // saveAs(blob, `exported-${unixNow()}.sigit.zip`) - // - // setIsLoading(false) - // - // navigate(appPublicRoutes.verify) - // } - const handleEncryptedExport = async () => { - if (Object.entries(files).length === 0 || !meta) return - - const stringifiedMeta = JSON.stringify(meta, null, 2) - const zip = await getZipWithFiles(meta, files) - - zip.file('meta.json', stringifiedMeta) - - const arrayBuffer = await zip - .generateAsync({ - type: 'arraybuffer', - compression: 'DEFLATE', - compressionOptions: { - level: 6 - } - }) - .catch((err) => { - console.log('err in zip:>> ', err) - setIsLoading(false) - toast.error(err.message || 'Error occurred in generating zip file') - return null - }) - + const arrayBuffer = await prepareZipExport() if (!arrayBuffer) return const key = await generateEncryptionKey() @@ -825,6 +722,72 @@ export const SignPage = () => { if (!finalZipFile) return saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`) + + setIsLoading(false) + } + + const prepareZipExport = async (): Promise => { + if (Object.entries(files).length === 0 || !meta || !usersPubkey) + return Promise.resolve(null) + + const usersNpub = hexToNpub(usersPubkey) + if ( + !signers.includes(usersNpub) && + !viewers.includes(usersNpub) && + submittedBy !== usersNpub + ) + return Promise.resolve(null) + + setIsLoading(true) + setLoadingSpinnerDesc('Signing nostr event') + + if (!meta) return Promise.resolve(null) + + const prevSig = getLastSignersSig(meta, signers) + if (!prevSig) return Promise.resolve(null) + + const signedEvent = await signEventForMetaFile( + JSON.stringify({ + prevSig + }), + nostrController, + setIsLoading + ) + + if (!signedEvent) return Promise.resolve(null) + + const exportSignature = JSON.stringify(signedEvent, null, 2) + + const stringifiedMeta = JSON.stringify( + { + ...meta, + exportSignature + }, + null, + 2 + ) + + const zip = await getZipWithFiles(meta, files) + zip.file('meta.json', stringifiedMeta) + + const arrayBuffer = await zip + .generateAsync({ + type: ARRAY_BUFFER, + compression: DEFLATE, + compressionOptions: { + level: 6 + } + }) + .catch((err) => { + console.log('err in zip:>> ', err) + setIsLoading(false) + toast.error(err.message || 'Error occurred in generating zip file') + return null + }) + + if (!arrayBuffer) return Promise.resolve(null) + + return Promise.resolve(arrayBuffer) } /** diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 1d91fa4..1aedc4d 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -26,7 +26,9 @@ import { sendNotification, generateEncryptionKey, encryptArrayBuffer, - generateKeysFile + generateKeysFile, + ARRAY_BUFFER, + DEFLATE } from '../../utils' import styles from './style.module.scss' import { useLocation, useParams } from 'react-router-dom' @@ -544,62 +546,6 @@ export const VerifyPage = () => { setIsLoading(false) } - const handleMarkedExport = async () => { - if (Object.entries(files).length === 0 || !meta || !usersPubkey) return - - const usersNpub = hexToNpub(usersPubkey) - if ( - !signers.includes(usersNpub) && - !viewers.includes(usersNpub) && - submittedBy !== usersNpub - ) { - return - } - - setIsLoading(true) - setLoadingSpinnerDesc('Signing nostr event') - - const prevSig = getLastSignersSig(meta, signers) - if (!prevSig) return - - const signedEvent = await signEventForMetaFile( - JSON.stringify({ prevSig }), - nostrController, - setIsLoading - ) - - if (!signedEvent) return - - const exportSignature = JSON.stringify(signedEvent, null, 2) - const updatedMeta = { ...meta, exportSignature } - const stringifiedMeta = JSON.stringify(updatedMeta, null, 2) - - const zip = await getZipWithFiles(updatedMeta, files) - zip.file('meta.json', stringifiedMeta) - - const arrayBuffer = await zip - .generateAsync({ - type: 'arraybuffer', - compression: 'DEFLATE', - compressionOptions: { - level: 6 - } - }) - .catch((err) => { - console.log('err in zip:>> ', err) - setIsLoading(false) - toast.error(err.message || 'Error occurred in generating zip file') - return null - }) - - if (!arrayBuffer) return - - const blob = new Blob([arrayBuffer]) - saveAs(blob, `exported-${unixNow()}.sigit.zip`) - - setIsLoading(false) - } - // Handle errors during zip file generation const handleZipError = (err: unknown) => { console.log('Error in zip:>> ', err) @@ -678,29 +624,18 @@ export const VerifyPage = () => { }) } + const handleExport = async () => { + const arrayBuffer = await prepareZipExport() + if (!arrayBuffer) return + + const blob = new Blob([arrayBuffer]) + saveAs(blob, `exported-${unixNow()}.sigit.zip`) + + setIsLoading(false) + } + const handleEncryptedExport = async () => { - if (Object.entries(files).length === 0 || !meta) return - - const stringifiedMeta = JSON.stringify(meta, null, 2) - const zip = await getZipWithFiles(meta, files) - - zip.file('meta.json', stringifiedMeta) - - const arrayBuffer = await zip - .generateAsync({ - type: 'arraybuffer', - compression: 'DEFLATE', - compressionOptions: { - level: 6 - } - }) - .catch((err) => { - console.log('err in zip:>> ', err) - setIsLoading(false) - toast.error(err.message || 'Error occurred in generating zip file') - return null - }) - + const arrayBuffer = await prepareZipExport() if (!arrayBuffer) return const key = await generateEncryptionKey() @@ -712,6 +647,62 @@ export const VerifyPage = () => { if (!finalZipFile) return saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`) + + setIsLoading(false) + } + + const prepareZipExport = async (): Promise => { + if (Object.entries(files).length === 0 || !meta || !usersPubkey) + return Promise.resolve(null) + + const usersNpub = hexToNpub(usersPubkey) + if ( + !signers.includes(usersNpub) && + !viewers.includes(usersNpub) && + submittedBy !== usersNpub + ) { + return Promise.resolve(null) + } + + setIsLoading(true) + setLoadingSpinnerDesc('Signing nostr event') + + const prevSig = getLastSignersSig(meta, signers) + if (!prevSig) return Promise.resolve(null) + + const signedEvent = await signEventForMetaFile( + JSON.stringify({ prevSig }), + nostrController, + setIsLoading + ) + + if (!signedEvent) return Promise.resolve(null) + + const exportSignature = JSON.stringify(signedEvent, null, 2) + const updatedMeta = { ...meta, exportSignature } + const stringifiedMeta = JSON.stringify(updatedMeta, null, 2) + + const zip = await getZipWithFiles(updatedMeta, files) + zip.file('meta.json', stringifiedMeta) + + const arrayBuffer = await zip + .generateAsync({ + type: ARRAY_BUFFER, + compression: DEFLATE, + compressionOptions: { + level: 6 + } + }) + .catch((err) => { + console.log('err in zip:>> ', err) + setIsLoading(false) + toast.error(err.message || 'Error occurred in generating zip file') + return null + }) + + if (!arrayBuffer) return Promise.resolve(null) + + return Promise.resolve(arrayBuffer) } return ( @@ -757,7 +748,7 @@ export const VerifyPage = () => { )} currentFile={currentFile} setCurrentFile={setCurrentFile} - handleExport={handleMarkedExport} + handleExport={handleExport} handleEncryptedExport={handleEncryptedExport} /> ) From 245585662a094da385702438bcdd72387cd5267f Mon Sep 17 00:00:00 2001 From: Stixx Date: Tue, 10 Dec 2024 14:46:26 +0100 Subject: [PATCH 20/39] fix: removed redundant variable --- src/pages/sign/index.tsx | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 16fa3c4..2c69adf 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -12,42 +12,40 @@ import { NostrController } from '../../controllers' import { appPrivateRoutes, appPublicRoutes } from '../../routes' import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types' import { + ARRAY_BUFFER, decryptArrayBuffer, + DEFLATE, encryptArrayBuffer, extractMarksFromSignedMeta, extractZipUrlAndEncryptionKey, + filterMarksByPubkey, + findOtherUserMarks, generateEncryptionKey, generateKeysFile, getCurrentUserFiles, + getCurrentUserMarks, getHash, hexToNpub, isOnline, loadZip, - unixNow, npubToHex, parseJson, + processMarks, readContentOfZipEntry, sendNotification, signEventForMetaFile, - updateUsersAppData, - findOtherUserMarks, timeout, - processMarks, - ARRAY_BUFFER + unixNow, + updateMarks, + updateUsersAppData } from '../../utils' import { CurrentUserMark, Mark } from '../../types/mark.ts' -import { - filterMarksByPubkey, - getCurrentUserMarks, - updateMarks -} from '../../utils' import PdfMarking from '../../components/PDFView/PdfMarking.tsx' import { convertToSigitFile, getZipWithFiles, SigitFile } from '../../utils/file.ts' -import { DEFLATE } from '../../utils' import { generateTimestamp } from '../../utils/opentimestamps.ts' import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx' import { getLastSignersSig } from '../../utils/sign.ts' @@ -195,11 +193,10 @@ export const SignPage = () => { m.value && encryptionKey ) { - const decrypted = await fetchAndDecrypt( + otherUserMarks[i].value = await fetchAndDecrypt( m.value, encryptionKey ) - otherUserMarks[i].value = decrypted } } catch (error) { console.error(`Error during mark fetchAndDecrypt phase`, error) From 99fa3add562c4efa013954c0e0df95723b9aea3d Mon Sep 17 00:00:00 2001 From: Stixx Date: Tue, 10 Dec 2024 15:29:24 +0100 Subject: [PATCH 21/39] fix: bug, when valid npub, clicking + was saying npub was invalid --- src/pages/create/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 1defde7..0ce990f 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -1162,7 +1162,9 @@ export const CreatePage = () => { ) : ( +
)} {complete && (
- +
)} @@ -148,6 +150,7 @@ const MarkFormField = ({ return (