diff --git a/package-lock.json b/package-lock.json index bf72f5f..366bbd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "lodash": "4.17.21", "mui-file-input": "4.0.4", "nostr-tools": "2.7.0", + "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.4.168", "react": "^18.2.0", "react-dnd": "16.0.1", @@ -1741,6 +1742,24 @@ } } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -4762,6 +4781,18 @@ "node": ">=6" } }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, "node_modules/pdfjs-dist": { "version": "4.4.168", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.4.168.tgz", @@ -5618,6 +5649,12 @@ "resolved": "https://registry.npmjs.org/tseep/-/tseep-1.2.1.tgz", "integrity": "sha512-VFnsNcPGC4qFJ1nxbIPSjTmtRZOhlqLmtwRqtLVos8mbRHki8HO9cy9Z1e89EiWyxFmq6LBviI9TQjijxw/mEw==" }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/tstl": { "version": "2.5.13", "resolved": "https://registry.npmjs.org/tstl/-/tstl-2.5.13.tgz", diff --git a/package.json b/package.json index 6984acc..0041c32 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "lodash": "4.17.21", "mui-file-input": "4.0.4", "nostr-tools": "2.7.0", + "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.4.168", "react": "^18.2.0", "react-dnd": "16.0.1", diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index c23bf70..148e70f 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -8,10 +8,11 @@ import { ProfileMetadata, User } from '../../types'; import { PdfFile, DrawTool, MouseState, PdfPage, DrawnField, MarkType } from '../../types/drawing'; import { truncate } from 'lodash'; import { hexToNpub } from '../../utils'; +import { toPdfFiles } from '../../utils/pdf.ts' PDFJS.GlobalWorkerOptions.workerSrc = 'node_modules/pdfjs-dist/build/pdf.worker.mjs'; interface Props { - selectedFiles: any[] + selectedFiles: File[] users: User[] metadata: { [key: string]: ProfileMetadata } onDrawFieldsChange: (pdfFiles: PdfFile[]) => void @@ -98,7 +99,7 @@ export const DrawPDFFields = (props: Props) => { * Fired only when left click and mouse over pdf page * Creates new drawnElement and pushes in the array * It is re rendered and visible right away - * + * * @param event Mouse event * @param page PdfPage where press happened */ @@ -138,7 +139,7 @@ export const DrawPDFFields = (props: Props) => { * Drawing is finished, resets all the variables used to draw * @param event Mouse event */ - const onMouseUp = (event: MouseEvent) => { + const onMouseUp = () => { setMouseState((prev) => { return { ...prev, @@ -182,11 +183,11 @@ export const DrawPDFFields = (props: Props) => { * so when we start moving, offset can be calculated * mouseX - offsetX * mouseY - offsetY - * + * * @param event Mouse event * @param drawnField Which we are moving */ - const onDrawnFieldMouseDown = (event: any, drawnField: DrawnField) => { + const onDrawnFieldMouseDown = (event: any) => { event.stopPropagation() // Proceed only if left click @@ -239,7 +240,7 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param drawnField which we are resizing */ - const onResizeHandleMouseDown = (event: any, drawnField: DrawnField) => { + const onResizeHandleMouseDown = (event: any) => { // Proceed only if left click if (event.button !== 0) return @@ -315,75 +316,13 @@ export const DrawPDFFields = (props: Props) => { * creates the pdfFiles object and sets to a state */ const parsePdfPages = async () => { - const pdfFiles: PdfFile[] = [] - - for (const file of selectedFiles) { - if (file.type.toLowerCase().includes('pdf')) { - const data = await readPdf(file) - const pages = await pdfToImages(data) - - pdfFiles.push({ - file: file, - pages: pages, - expanded: false - }) - } - } + const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles); setPdfFiles(pdfFiles) } /** - * Converts pdf to the images - * @param data pdf file bytes - */ - const pdfToImages = async (data: any): Promise => { - const images: string[] = []; - const pdf = await PDFJS.getDocument(data).promise; - const canvas = document.createElement("canvas"); - - for (let i = 0; i < pdf.numPages; i++) { - const page = await pdf.getPage(i + 1); - const viewport = page.getViewport({ scale: 3 }); - const context = canvas.getContext("2d"); - canvas.height = viewport.height; - canvas.width = viewport.width; - await page.render({ canvasContext: context!, viewport: viewport }).promise; - images.push(canvas.toDataURL()); - } - - return Promise.resolve(images.map((image) => { - return { - image, - drawnFields: [] - } - })) - } - - /** - * Reads the pdf file binaries - */ - const readPdf = (file: File) => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - - reader.onload = (e: any) => { - const data = e.target.result - - resolve(data) - }; - - reader.onerror = (err) => { - console.error('err', err) - reject(err) - }; - - reader.readAsDataURL(file); - }) - } - - /** - * + * * @returns if expanded pdf accordion is present */ const hasExpandedPdf = () => { @@ -437,7 +376,7 @@ export const DrawPDFFields = (props: Props) => { return (
{ onDrawnFieldMouseDown(event, drawnField) }} + onMouseDown={onDrawnFieldMouseDown} onMouseMove={(event) => { onDranwFieldMouseMove(event, drawnField)}} className={styles.drawingRectangle} style={{ @@ -449,7 +388,7 @@ export const DrawPDFFields = (props: Props) => { }} > {onResizeHandleMouseDown(event, drawnField)}} + onMouseDown={onResizeHandleMouseDown} onMouseMove={(event) => {onResizeHandleMouseMove(event, drawnField)}} className={styles.resizeHandle} > @@ -460,7 +399,7 @@ export const DrawPDFFields = (props: Props) => {
{onUserSelectHandleMouseDown(event)}} + onMouseDown={onUserSelectHandleMouseDown} className={styles.userSelect} > diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 4793f44..e3e7856 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -59,13 +59,17 @@ .drawingRectangle { position: absolute; border: 1px solid #01aaad; - width: 40px; - height: 40px; z-index: 50; background-color: #01aaad4b; cursor: pointer; display: flex; justify-content: center; + align-items: center; + + &.nonEditable { + cursor: default; + visibility: hidden; + } .resizeHandle { position: absolute; diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx new file mode 100644 index 0000000..eb5ceff --- /dev/null +++ b/src/components/PDFView/PdfItem.tsx @@ -0,0 +1,35 @@ +import { PdfFile } from '../../types/drawing.ts' +import { CurrentUserMark } from '../../types/mark.ts' +import PdfPageItem from './PdfPageItem.tsx'; + +interface PdfItemProps { + pdfFile: PdfFile + currentUserMarks: CurrentUserMark[] + handleMarkClick: (id: number) => void + selectedMarkValue: string + selectedMark: CurrentUserMark | null +} + +/** + * Responsible for displaying pages of a single Pdf File. + */ +const PdfItem = ({ pdfFile, currentUserMarks, handleMarkClick, selectedMarkValue, selectedMark }: PdfItemProps) => { + const filterByPage = (marks: CurrentUserMark[], page: number): CurrentUserMark[] => { + return marks.filter((m) => m.mark.location.page === page); + } + return ( + pdfFile.pages.map((page, i) => { + return ( + + ) + })) +} + +export default PdfItem \ No newline at end of file diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx new file mode 100644 index 0000000..7a2b24b --- /dev/null +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -0,0 +1,37 @@ +import { CurrentUserMark } from '../../types/mark.ts' +import styles from '../DrawPDFFields/style.module.scss' +import { inPx } from '../../utils/pdf.ts' + +interface PdfMarkItemProps { + userMark: CurrentUserMark + handleMarkClick: (id: number) => void + selectedMarkValue: string + selectedMark: CurrentUserMark | null +} + +/** + * Responsible for display an individual Pdf Mark. + */ +const PdfMarkItem = ({ selectedMark, handleMarkClick, selectedMarkValue, userMark }: PdfMarkItemProps) => { + const { location } = userMark.mark; + const handleClick = () => handleMarkClick(userMark.mark.id); + const getMarkValue = () => ( + selectedMark?.mark.id === userMark.mark.id + ? selectedMarkValue + : userMark.mark.value + ) + return ( +
{getMarkValue()}
+ ) +} + +export default PdfMarkItem \ No newline at end of file diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx new file mode 100644 index 0000000..8de12f0 --- /dev/null +++ b/src/components/PDFView/PdfMarking.tsx @@ -0,0 +1,101 @@ +import { Box } from '@mui/material' +import styles from '../../pages/sign/style.module.scss' +import PdfView from './index.tsx' +import MarkFormField from '../../pages/sign/MarkFormField.tsx' +import { PdfFile } from '../../types/drawing.ts' +import { CurrentUserMark, Mark } from '../../types/mark.ts' +import React, { useState, useEffect } from 'react' +import { + findNextCurrentUserMark, + isCurrentUserMarksComplete, + updateCurrentUserMarks, +} from '../../utils/mark.ts' + +import { EMPTY } from '../../utils/const.ts' + +interface PdfMarkingProps { + files: { pdfFile: PdfFile, filename: string, hash: string | null }[], + currentUserMarks: CurrentUserMark[], + setIsReadyToSign: (isReadyToSign: boolean) => void, + setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void, + setUpdatedMarks: (markToUpdate: Mark) => void +} + +/** + * Top-level component responsible for displaying Pdfs, Pages, and Marks, + * as well as tracking if the document is ready to be signed. + * @param props + * @constructor + */ +const PdfMarking = (props: PdfMarkingProps) => { + const { + files, + currentUserMarks, + setIsReadyToSign, + setCurrentUserMarks, + setUpdatedMarks + } = props + const [selectedMark, setSelectedMark] = useState(null) + const [selectedMarkValue, setSelectedMarkValue] = useState("") + + useEffect(() => { + setSelectedMark(findNextCurrentUserMark(currentUserMarks) || null) + }, [currentUserMarks]) + + const handleMarkClick = (id: number) => { + const nextMark = currentUserMarks.find((mark) => mark.mark.id === id); + setSelectedMark(nextMark!); + setSelectedMarkValue(nextMark?.mark.value ?? EMPTY); + } + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + if (!selectedMarkValue || !selectedMark) return; + + const updatedMark: CurrentUserMark = { + ...selectedMark, + mark: { + ...selectedMark.mark, + value: selectedMarkValue + }, + isCompleted: true + } + + setSelectedMarkValue(EMPTY) + const updatedCurrentUserMarks = updateCurrentUserMarks(currentUserMarks, updatedMark); + setCurrentUserMarks(updatedCurrentUserMarks) + setSelectedMark(findNextCurrentUserMark(updatedCurrentUserMarks) || null) + console.log(isCurrentUserMarksComplete(updatedCurrentUserMarks)) + setIsReadyToSign(isCurrentUserMarksComplete(updatedCurrentUserMarks)) + setUpdatedMarks(updatedMark.mark) + } + + const handleChange = (event: React.ChangeEvent) => setSelectedMarkValue(event.target.value) + + return ( + <> + + { + currentUserMarks?.length > 0 && ( + )} + { + selectedMark !== null && ( + + )} + + + ) +} + +export default PdfMarking \ No newline at end of file diff --git a/src/components/PDFView/PdfPageItem.tsx b/src/components/PDFView/PdfPageItem.tsx new file mode 100644 index 0000000..d289a6e --- /dev/null +++ b/src/components/PDFView/PdfPageItem.tsx @@ -0,0 +1,46 @@ +import styles from '../DrawPDFFields/style.module.scss' +import { PdfPage } from '../../types/drawing.ts' +import { CurrentUserMark } from '../../types/mark.ts' +import PdfMarkItem from './PdfMarkItem.tsx' +interface PdfPageProps { + page: PdfPage + currentUserMarks: CurrentUserMark[] + handleMarkClick: (id: number) => void + selectedMarkValue: string + selectedMark: CurrentUserMark | null +} + +/** + * Responsible for rendering a single Pdf Page and its Marks + */ +const PdfPageItem = ({ page, currentUserMarks, handleMarkClick, selectedMarkValue, selectedMark }: PdfPageProps) => { + return ( +
+ + { + currentUserMarks.map((m, i) => ( + + ))} +
+ ) +} + +export default PdfPageItem \ No newline at end of file diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx new file mode 100644 index 0000000..14834a3 --- /dev/null +++ b/src/components/PDFView/index.tsx @@ -0,0 +1,42 @@ +import { PdfFile } from '../../types/drawing.ts' +import { Box } from '@mui/material' +import PdfItem from './PdfItem.tsx' +import { CurrentUserMark } from '../../types/mark.ts' + +interface PdfViewProps { + files: { pdfFile: PdfFile, filename: string, hash: string | null }[] + currentUserMarks: CurrentUserMark[] + handleMarkClick: (id: number) => void + selectedMarkValue: string + selectedMark: CurrentUserMark | null +} + +/** + * Responsible for rendering Pdf files. + */ +const PdfView = ({ files, currentUserMarks, handleMarkClick, selectedMarkValue, selectedMark }: PdfViewProps) => { + const filterByFile = (currentUserMarks: CurrentUserMark[], hash: string): CurrentUserMark[] => { + return currentUserMarks.filter((currentUserMark) => currentUserMark.mark.pdfFileHash === hash) + } + return ( + + { + files.map(({ pdfFile, hash }, i) => { + if (!hash) return + return ( + + ) + }) + } + + ) +} + +export default PdfView; \ No newline at end of file diff --git a/src/components/PDFView/style.module.scss b/src/components/PDFView/style.module.scss new file mode 100644 index 0000000..2e6e519 --- /dev/null +++ b/src/components/PDFView/style.module.scss @@ -0,0 +1,16 @@ +.imageWrapper { + display: flex; + justify-content: center; + align-items: center; + width: 100%; /* Adjust as needed */ + height: 100%; /* Adjust as needed */ + overflow: hidden; /* Ensure no overflow */ + border: 1px solid #ccc; /* Optional: for visual debugging */ + background-color: #e0f7fa; /* Optional: for visual debugging */ +} + +.image { + max-width: 100%; + max-height: 100%; + object-fit: contain; /* Ensure the image fits within the container */ +} \ No newline at end of file diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 13e655a..7224a74 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -62,6 +62,7 @@ import { import styles from './style.module.scss' import { PdfFile } from '../../types/drawing' import { DrawPDFFields } from '../../components/DrawPDFFields' +import { Mark } from '../../types/mark.ts' export const CreatePage = () => { const navigate = useNavigate() @@ -339,26 +340,29 @@ export const CreatePage = () => { return fileHashes } - const createMarkConfig = (fileHashes: { [key: string]: string }) => { - let markConfig: any = {} - - drawnPdfs.forEach(drawnPdf => { - const fileHash = fileHashes[drawnPdf.file.name] - - drawnPdf.pages.forEach((page, pageIndex) => { - page.drawnFields.forEach(drawnField => { - if (!markConfig[drawnField.counterpart]) markConfig[drawnField.counterpart] = {} - if (!markConfig[drawnField.counterpart][fileHash]) markConfig[drawnField.counterpart][fileHash] = [] - - markConfig[drawnField.counterpart][fileHash].push({ - markType: drawnField.type, - markLocation: `P:${pageIndex};X:${drawnField.left};Y:${drawnField.top}` - }) + const createMarks = (fileHashes: { [key: string]: string }) : Mark[] => { + return drawnPdfs.flatMap((drawnPdf) => { + const fileHash = fileHashes[drawnPdf.file.name]; + return drawnPdf.pages.flatMap((page, index) => { + return page.drawnFields.map((drawnField) => { + return { + type: drawnField.type, + location: { + page: index, + top: drawnField.top, + left: drawnField.left, + height: drawnField.height, + width: drawnField.width, + }, + npub: drawnField.counterpart, + pdfFileHash: fileHash + } }) }) }) - - return markConfig + .map((mark, index) => { + return {...mark, id: index } + }); } // Handle errors during zip file generation @@ -373,15 +377,13 @@ export const CreatePage = () => { const generateZipFile = async (zip: JSZip): Promise => { setLoadingSpinnerDesc('Generating zip file') - const arraybuffer = await zip + return await zip .generateAsync({ type: 'arraybuffer', compression: 'DEFLATE', compressionOptions: { level: 6 } }) .catch(handleZipError) - - return arraybuffer } // Encrypt the zip file with the generated encryption key @@ -428,15 +430,13 @@ export const CreatePage = () => { if (!arraybuffer) return null - const finalZipFile = new File( + return new File( [new Blob([arraybuffer])], `${unixNow}.sigit.zip`, { type: 'application/zip' } ) - - return finalZipFile } // Handle errors during file upload @@ -458,14 +458,12 @@ export const CreatePage = () => { type: 'application/sigit' }) - const fileUrl = await uploadToFileStorage(file) + return await uploadToFileStorage(file) .then((url) => { toast.success('files.zip uploaded to file storage') return url }) .catch(handleUploadError) - - return fileUrl } // Manage offline scenarios for signing or viewing the file @@ -489,15 +487,13 @@ export const CreatePage = () => { zip.file(file.name, file) }) - const arraybuffer = await zip + return await zip .generateAsync({ type: 'arraybuffer', compression: 'DEFLATE', compressionOptions: { level: 6 } }) .catch(handleZipError) - - return arraybuffer } const generateCreateSignature = async ( @@ -508,7 +504,7 @@ export const CreatePage = () => { ) => { const signers = users.filter((user) => user.role === UserRole.signer) const viewers = users.filter((user) => user.role === UserRole.viewer) - const markConfig = createMarkConfig(fileHashes) + const markConfig = createMarks(fileHashes) const content: CreateSignatureEventContent = { signers: signers.map((signer) => hexToNpub(signer.pubkey)), @@ -548,11 +544,9 @@ export const CreatePage = () => { : viewers.map((viewer) => viewer.pubkey) ).filter((receiver) => receiver !== usersPubkey) - const promises = receivers.map((receiver) => + return receivers.map((receiver) => sendNotification(receiver, meta) ) - - return promises } const handleCreate = async () => { diff --git a/src/pages/sign/MarkFormField.tsx b/src/pages/sign/MarkFormField.tsx new file mode 100644 index 0000000..4de82a4 --- /dev/null +++ b/src/pages/sign/MarkFormField.tsx @@ -0,0 +1,37 @@ +import { CurrentUserMark } from '../../types/mark.ts' +import styles from './style.module.scss' +import { Box, Button, TextField } from '@mui/material' + +import { MARK_TYPE_TRANSLATION } from '../../utils/const.ts' + +interface MarkFormFieldProps { + handleSubmit: (event: any) => void + handleChange: (event: any) => void + selectedMark: CurrentUserMark + selectedMarkValue: string +} + +/** + * Responsible for rendering a form field connected to a mark and keeping track of its value. + */ +const MarkFormField = (props: MarkFormFieldProps) => { + const { handleSubmit, handleChange, selectedMark, selectedMarkValue } = props; + const getSubmitButton = () => selectedMark.isLast ? 'Complete' : 'Next'; + return ( +
+ + + + +
+ ) +} + +export default MarkFormField; \ No newline at end of file diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 98ab74e..d7de575 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -16,13 +16,13 @@ import { State } from '../../store/rootReducer' import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types' import { decryptArrayBuffer, - encryptArrayBuffer, + encryptArrayBuffer, extractMarksFromSignedMeta, extractZipUrlAndEncryptionKey, generateEncryptionKey, - generateKeysFile, + generateKeysFile, getFilesWithHashes, getHash, hexToNpub, - isOnline, + isOnline, loadZip, now, npubToHex, parseJson, @@ -33,6 +33,17 @@ import { } from '../../utils' import { DisplayMeta } from './internal/displayMeta' import styles from './style.module.scss' +import { PdfFile } from '../../types/drawing.ts' +import { convertToPdfFile } from '../../utils/pdf.ts' +// import PdfView from '../../components/PDFView' +import { CurrentUserMark, Mark } from '../../types/mark.ts' +import { getLastSignersSig } from '../../utils/sign.ts' +import { + filterMarksByPubkey, + getCurrentUserMarks, + isCurrentUserMarksComplete, updateMarks +} from '../../utils' +import PdfMarking from '../../components/PDFView/PdfMarking.tsx' import { Container } from '../../components/Container' enum SignedStatus { Fully_Signed, @@ -59,7 +70,7 @@ export const SignPage = () => { const [selectedFile, setSelectedFile] = useState(null) - const [files, setFiles] = useState<{ [filename: string]: ArrayBuffer }>({}) + const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -71,6 +82,7 @@ export const SignPage = () => { const [signers, setSigners] = useState<`npub1${string}`[]>([]) const [viewers, setViewers] = useState<`npub1${string}`[]>([]) + const [marks, setMarks] = useState([]) const [creatorFileHashes, setCreatorFileHashes] = useState<{ [key: string]: string }>({}) @@ -89,6 +101,8 @@ export const SignPage = () => { const [authUrl, setAuthUrl] = useState() const nostrController = NostrController.getInstance() + const [currentUserMarks, setCurrentUserMarks] = useState([]); + const [isReadyToSign, setIsReadyToSign] = useState(false); useEffect(() => { if (signers.length > 0) { @@ -179,6 +193,16 @@ export const SignPage = () => { setViewers(createSignatureContent.viewers) setCreatorFileHashes(createSignatureContent.fileHashes) setSubmittedBy(createSignatureEvent.pubkey) + setMarks(createSignatureContent.markConfig); + + if (usersPubkey) { + const metaMarks = filterMarksByPubkey(createSignatureContent.markConfig, usersPubkey!) + const signedMarks = extractMarksFromSignedMeta(meta) + const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks); + setCurrentUserMarks(currentUserMarks); + // setCurrentUserMark(findNextCurrentUserMark(currentUserMarks) || null) + setIsReadyToSign(isCurrentUserMarksComplete(currentUserMarks)) + } setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[]) } @@ -189,6 +213,7 @@ export const SignPage = () => { }, [meta]) useEffect(() => { + // online mode - from create and home page views if (metaInNavState) { const processSigit = async () => { setIsLoading(true) @@ -263,16 +288,13 @@ export const SignPage = () => { if (!decrypted) return - const zip = await JSZip.loadAsync(decrypted).catch((err) => { - console.log('err in loading zip file :>> ', err) - toast.error(err.message || 'An error occurred in loading zip file.') + const zip = await loadZip(decrypted) + if (!zip) { setIsLoading(false) - return null - }) + return + } - if (!zip) return - - const files: { [filename: string]: ArrayBuffer } = {} + const files: { [filename: string]: PdfFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files).map((entry) => entry.name) @@ -286,7 +308,7 @@ export const SignPage = () => { ) if (arrayBuffer) { - files[fileName] = arrayBuffer + files[fileName] = await convertToPdfFile(arrayBuffer, fileName); const hash = await getHash(arrayBuffer) if (hash) { @@ -301,6 +323,11 @@ export const SignPage = () => { setCurrentFileHashes(fileHashes) } + const setUpdatedMarks = (markToUpdate: Mark) => { + const updatedMarks = updateMarks(marks, markToUpdate) + setMarks(updatedMarks) + } + const parseKeysJson = async (zip: JSZip) => { const keysFileContent = await readContentOfZipEntry( zip, @@ -310,25 +337,19 @@ export const SignPage = () => { if (!keysFileContent) return null - const parsedJSON = await parseJson<{ sender: string; keys: string[] }>( + return await parseJson<{ sender: string; keys: string[] }>( keysFileContent ).catch((err) => { console.log(`Error parsing content of keys.json:`, err) toast.error(err.message || `Error parsing content of keys.json`) return null }) - - return parsedJSON } const decrypt = async (file: File) => { setLoadingSpinnerDesc('Decrypting file') - const zip = await JSZip.loadAsync(file).catch((err) => { - console.log('err in loading zip file :>> ', err) - toast.error(err.message || 'An error occurred in loading zip file.') - return null - }) + const zip = await loadZip(file); if (!zip) return const parsedKeysJson = await parseKeysJson(zip) @@ -399,32 +420,27 @@ export const SignPage = () => { setLoadingSpinnerDesc('Parsing zip file') - const zip = await JSZip.loadAsync(decryptedZipFile).catch((err) => { - console.log('err in loading zip file :>> ', err) - toast.error(err.message || 'An error occurred in loading zip file.') - return null - }) - + const zip = await loadZip(decryptedZipFile) if (!zip) return - const files: { [filename: string]: ArrayBuffer } = {} + const files: { [filename: string]: PdfFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files) .filter((entry) => entry.name.startsWith('files/') && !entry.dir) .map((entry) => entry.name) + .map((entry) => entry.replace(/^files\//, '')) // generate hashes for all entries in files folder of zipArchive // these hashes can be used to verify the originality of files - for (let fileName of fileNames) { + for (const fileName of fileNames) { const arrayBuffer = await readContentOfZipEntry( zip, fileName, 'arraybuffer' ) - fileName = fileName.replace(/^files\//, '') if (arrayBuffer) { - files[fileName] = arrayBuffer + files[fileName] = await convertToPdfFile(arrayBuffer, fileName); const hash = await getHash(arrayBuffer) if (hash) { @@ -488,7 +504,10 @@ export const SignPage = () => { const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!)) if (!prevSig) return - const signedEvent = await signEventForMeta(prevSig) + const marks = getSignerMarksForMeta() + if (!marks) return + + const signedEvent = await signEventForMeta({ prevSig, marks }) if (!signedEvent) return const updatedMeta = updateMetaSignatures(meta, signedEvent) @@ -502,14 +521,19 @@ export const SignPage = () => { } // Sign the event for the meta file - const signEventForMeta = async (prevSig: string) => { + const signEventForMeta = async (signerContent: { prevSig: string, marks: Mark[] }) => { return await signEventForMetaFile( - JSON.stringify({ prevSig }), + JSON.stringify(signerContent), nostrController, setIsLoading ) } + const getSignerMarksForMeta = (): Mark[] | undefined => { + if (currentUserMarks.length === 0) return; + return currentUserMarks.map(( { mark }: CurrentUserMark) => mark); + } + // Update the meta signatures const updateMetaSignatures = (meta: Meta, signedEvent: SignedEvent): Meta => { const metaCopy = _.cloneDeep(meta) @@ -527,7 +551,7 @@ export const SignPage = () => { encryptionKey: string ): Promise => { // Get the current timestamp in seconds - const unixNow = Math.floor(Date.now() / 1000) + const unixNow = now() const blob = new Blob([encryptedArrayBuffer]) // Create a File object with the Blob data const file = new File([blob], `compressed.sigit`, { @@ -577,15 +601,13 @@ export const SignPage = () => { if (!arraybuffer) return null - const finalZipFile = new File( + return new File( [new Blob([arraybuffer])], `${unixNow}.sigit.zip`, { type: 'application/zip' } ) - - return finalZipFile } // Handle errors during zip file generation @@ -673,7 +695,9 @@ export const SignPage = () => { setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') - const prevSig = getLastSignersSig() + if (!meta) return; + + const prevSig = getLastSignersSig(meta, signers) if (!prevSig) return const signedEvent = await signEventForMetaFile( @@ -723,7 +747,7 @@ export const SignPage = () => { if (!arrayBuffer) return const blob = new Blob([arrayBuffer]) - const unixNow = Math.floor(Date.now() / 1000) + const unixNow = now() saveAs(blob, `exported-${unixNow}.sigit.zip`) setIsLoading(false) @@ -769,7 +793,7 @@ export const SignPage = () => { const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key) if (!finalZipFile) return - const unixNow = Math.floor(Date.now() / 1000) + const unixNow = now() saveAs(finalZipFile, `exported-${unixNow}.sigit.zip`) } @@ -806,37 +830,6 @@ export const SignPage = () => { } } - /** - * This function returns the signature of last signer - * It will be used in the content of export signature's signedEvent - */ - const getLastSignersSig = () => { - if (!meta) return null - - // if there're no signers then use creator's signature - if (signers.length === 0) { - try { - const createSignatureEvent: Event = JSON.parse(meta.createSignature) - return createSignatureEvent.sig - } catch (error) { - return null - } - } - - // get last signer - const lastSigner = signers[signers.length - 1] - - // get the signature of last signer - try { - const lastSignatureEvent: Event = JSON.parse( - meta.docSignatures[lastSigner] - ) - return lastSignatureEvent.sig - } catch (error) { - return null - } - } - if (authUrl) { return (