From 54a60d4a5a9f940e18cccab7032dba07363ca39d Mon Sep 17 00:00:00 2001 From: Stixx Date: Fri, 12 Jul 2024 15:07:36 +0200 Subject: [PATCH 01/17] chore: added comments --- src/components/DrawPDFFields/index.tsx | 73 ++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 565f43a..c23bf70 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -94,6 +94,14 @@ export const DrawPDFFields = (props: Props) => { setPdfFiles([...pdfFiles]) } + /** + * 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 + */ const onMouseDown = (event: any, page: PdfPage) => { // Proceed only if left click if (event.button !== 0) return @@ -126,6 +134,10 @@ export const DrawPDFFields = (props: Props) => { }) } + /** + * Drawing is finished, resets all the variables used to draw + * @param event Mouse event + */ const onMouseUp = (event: MouseEvent) => { setMouseState((prev) => { return { @@ -137,6 +149,12 @@ export const DrawPDFFields = (props: Props) => { }) } + /** + * After {@link onMouseDown} create an drawing element, this function gets called on every pixel moved + * which alters the newly created drawing element, resizing it while mouse move + * @param event Mouse event + * @param page PdfPage where moving is happening + */ const onMouseMove = (event: any, page: PdfPage) => { if (mouseState.clicked && selectedTool) { const lastElementIndex = page.drawnFields.length -1 @@ -158,6 +176,16 @@ export const DrawPDFFields = (props: Props) => { } } + /** + * Fired when event happens on the drawn element which will be moved + * mouse coordinates relative to drawn element will be stored + * 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) => { event.stopPropagation() @@ -176,6 +204,11 @@ export const DrawPDFFields = (props: Props) => { }) } + /** + * Moves the drawnElement by the mouse position (mouse can grab anywhere on the drawn element) + * @param event Mouse event + * @param drawnField which we are moving + */ const onDranwFieldMouseMove = (event: any, drawnField: DrawnField) => { if (mouseState.dragging) { const { mouseX, mouseY, rect } = getMouseCoordinates(event, event.target.parentNode) @@ -201,6 +234,11 @@ export const DrawPDFFields = (props: Props) => { } } + /** + * Fired when clicked on the resize handle, sets the state for a resize action + * @param event Mouse event + * @param drawnField which we are resizing + */ const onResizeHandleMouseDown = (event: any, drawnField: DrawnField) => { // Proceed only if left click if (event.button !== 0) return @@ -212,6 +250,11 @@ export const DrawPDFFields = (props: Props) => { }) } + /** + * Resizes the drawn element by the mouse position + * @param event Mouse event + * @param drawnField which we are resizing + */ const onResizeHandleMouseMove = (event: any, drawnField: DrawnField) => { if (mouseState.resizing) { const { mouseX, mouseY } = getMouseCoordinates(event, event.target.parentNode.parentNode) @@ -226,12 +269,24 @@ export const DrawPDFFields = (props: Props) => { } } + /** + * Removes the drawn element using the indexes in the params + * @param event Mouse event + * @param pdfFileIndex pdf file index + * @param pdfPageIndex pdf page index + * @param drawnFileIndex drawn file index + */ const onRemoveHandleMouseDown = (event: any, pdfFileIndex: number, pdfPageIndex: number, drawnFileIndex: number) => { event.stopPropagation() pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice(drawnFileIndex, 1) } + /** + * Used to stop mouse click propagating to the parent elements + * so select can work properly + * @param event Mouse event + */ const onUserSelectHandleMouseDown = (event: any) => { event.stopPropagation() } @@ -255,6 +310,10 @@ export const DrawPDFFields = (props: Props) => { } } + /** + * Reads the pdf binary files and converts it's pages to images + * creates the pdfFiles object and sets to a state + */ const parsePdfPages = async () => { const pdfFiles: PdfFile[] = [] @@ -301,6 +360,9 @@ export const DrawPDFFields = (props: Props) => { })) } + /** + * Reads the pdf file binaries + */ const readPdf = (file: File) => { return new Promise((resolve, reject) => { const reader = new FileReader(); @@ -320,6 +382,10 @@ export const DrawPDFFields = (props: Props) => { }) } + /** + * + * @returns if expanded pdf accordion is present + */ const hasExpandedPdf = () => { return !!pdfFiles.filter(pdfFile => !!pdfFile.expanded).length } @@ -331,6 +397,10 @@ export const DrawPDFFields = (props: Props) => { setShowDrawToolBox(hasExpandedPdf()) } + /** + * Changes the drawing tool + * @param drawTool to draw with + */ const handleToolSelect = (drawTool: DrawTool) => { // If clicked on the same tool, unselect if (drawTool.identifier === selectedTool?.identifier) { @@ -341,6 +411,9 @@ export const DrawPDFFields = (props: Props) => { setSelectedTool(drawTool) } + /** + * Renders the pdf pages and drawing elements + */ const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => { return ( Date: Tue, 16 Jul 2024 12:36:18 +0300 Subject: [PATCH 02/17] chore: add pdf util --- src/components/DrawPDFFields/index.tsx | 68 ++-------------------- src/pages/create/index.tsx | 2 +- src/utils/pdf.ts | 78 ++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 65 deletions(-) create mode 100644 src/utils/pdf.ts diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index c23bf70..c582d2d 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 @@ -315,73 +316,12 @@ 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); + console.log('pdf files: ', pdfFiles); 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 diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 63df583..e914172 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -340,7 +340,7 @@ export const CreatePage = () => { } const createMarkConfig = (fileHashes: { [key: string]: string }) => { - let markConfig: any = {} + const markConfig: any = {} drawnPdfs.forEach(drawnPdf => { const fileHash = fileHashes[drawnPdf.file.name] diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts new file mode 100644 index 0000000..a73f39f --- /dev/null +++ b/src/utils/pdf.ts @@ -0,0 +1,78 @@ +import { PdfFile, PdfPage } from '../types/drawing.ts' +import * as PDFJS from 'pdfjs-dist' +PDFJS.GlobalWorkerOptions.workerSrc = 'node_modules/pdfjs-dist/build/pdf.worker.mjs'; + +const toFile = (arrayBuffer: ArrayBuffer, fileName: string) : File => { + const blob = new Blob([arrayBuffer], { type: "application/pdf" }); + return new File([blob], fileName, { type: "application/pdf" }); +} + +const toPdfFile = async (file: File): Promise => { + const data = await readPdf(file) + const pages = await pdfToImages(data) + return { file, pages, expanded: false } +} + +const toPdfFiles = async (selectedFiles: File[]): Promise => { + return Promise.all( + selectedFiles + .filter(isPdf) + .map(toPdfFile)); +} + +const isPdf = (file: File) => file.type.toLowerCase().includes('pdf'); + +/** + * 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); + }) +} + +/** + * 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: [] + } + })) +} + +export { + toFile, + toPdfFile, + toPdfFiles +} \ No newline at end of file From b58ba625f9087c74e81217f3418201673654524f Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 17 Jul 2024 11:25:02 +0300 Subject: [PATCH 03/17] feat(pdf-marking): add pdf-view components --- src/components/DrawPDFFields/index.tsx | 4 +- src/components/PDFView/Mark.tsx | 0 src/components/PDFView/PdfItem.tsx | 28 ++++++++++ src/components/PDFView/PdfMarkItem.tsx | 23 +++++++++ src/components/PDFView/PdfPageItem.tsx | 30 +++++++++++ src/components/PDFView/index.tsx | 45 ++++++++++++++++ src/pages/create/index.tsx | 14 ++++- src/pages/sign/index.tsx | 68 +++++++++++++++---------- src/pages/sign/internal/displayMeta.tsx | 5 +- src/pages/sign/style.module.scss | 32 ++++++++++++ src/types/mark.ts | 10 +++- src/types/zip.ts | 15 +++++- src/utils/pdf.ts | 5 +- src/utils/zip.ts | 21 +++++++- 14 files changed, 264 insertions(+), 36 deletions(-) create mode 100644 src/components/PDFView/Mark.tsx create mode 100644 src/components/PDFView/PdfItem.tsx create mode 100644 src/components/PDFView/PdfMarkItem.tsx create mode 100644 src/components/PDFView/PdfPageItem.tsx create mode 100644 src/components/PDFView/index.tsx diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index c582d2d..7d25996 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -101,7 +101,7 @@ export const DrawPDFFields = (props: Props) => { * It is re rendered and visible right away * * @param event Mouse event - * @param page PdfPage where press happened + * @param page PdfItem where press happened */ const onMouseDown = (event: any, page: PdfPage) => { // Proceed only if left click @@ -154,7 +154,7 @@ export const DrawPDFFields = (props: Props) => { * After {@link onMouseDown} create an drawing element, this function gets called on every pixel moved * which alters the newly created drawing element, resizing it while mouse move * @param event Mouse event - * @param page PdfPage where moving is happening + * @param page PdfItem where moving is happening */ const onMouseMove = (event: any, page: PdfPage) => { if (mouseState.clicked && selectedTool) { diff --git a/src/components/PDFView/Mark.tsx b/src/components/PDFView/Mark.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx new file mode 100644 index 0000000..7ffd000 --- /dev/null +++ b/src/components/PDFView/PdfItem.tsx @@ -0,0 +1,28 @@ +import { PdfFile } from '../../types/drawing.ts' +import { MarkConfigDetails} from '../../types/mark.ts' +import PdfPageItem from './PdfPageItem.tsx'; + +interface PdfItemProps { + pdfFile: PdfFile + markConfigDetails: MarkConfigDetails[] +} + +const PdfItem = ({ pdfFile, markConfigDetails }: PdfItemProps) => { + const filterMarkConfigDetails = (i: number) => { + return markConfigDetails.filter( + (details) => details.markLocation.page === i); + } + return ( + pdfFile.pages.map((page, i) => { + console.log('page: ', page); + 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..a170c45 --- /dev/null +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -0,0 +1,23 @@ +import { MarkLocation } from '../../types/mark.ts' +import styles from '../DrawPDFFields/style.module.scss' +import { inPx } from '../../utils/pdf.ts' + +interface PdfMarkItemProps { + markLocation: MarkLocation +} + +const PdfMarkItem = ({ markLocation }: PdfMarkItemProps) => { + return ( +
+ ) +} + +export default PdfMarkItem \ 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..5176fe9 --- /dev/null +++ b/src/components/PDFView/PdfPageItem.tsx @@ -0,0 +1,30 @@ +import styles from '../DrawPDFFields/style.module.scss' +import { PdfPage } from '../../types/drawing.ts' +import { MarkConfigDetails, MarkLocation } from '../../types/mark.ts' +import PdfMarkItem from './PdfMarkItem.tsx' +import { useState } from 'react'; +interface PdfPageProps { + page: PdfPage + markConfigDetails: MarkConfigDetails[] +} + +const PdfPageItem = ({ page, markConfigDetails }: PdfPageProps) => { + const [currentMark, setCurrentMark] = useState(null); + + return ( +
+ + {markConfigDetails.map((detail, 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..6c6113c --- /dev/null +++ b/src/components/PDFView/index.tsx @@ -0,0 +1,45 @@ +import { PdfFile } from '../../types/drawing.ts' +import { Box } from '@mui/material' +import PdfItem from './PdfItem.tsx' +import { MarkConfig, MarkConfigDetails } from '../../types/mark.ts' +import { State } from '../../store/rootReducer' +import { useSelector } from 'react-redux'; +import { hexToNpub, npubToHex } from '../../utils' + +interface PdfViewProps { + files: { [filename: string]: PdfFile }, + fileHashes: { [key: string]: string | null }, + markConfig: MarkConfig, +} + +const PdfView = (props: PdfViewProps) => { + console.log('current file hashes: ', props.fileHashes) + const usersPubkey = useSelector((state: State) => state.auth.usersPubkey); + if (!usersPubkey) return; + console.log(props.markConfig[hexToNpub(usersPubkey)]); + + console.log('users pubkey: ', usersPubkey); + console.log('mark config: ', props.markConfig); + + const getMarkConfigDetails = (fileName: string): MarkConfigDetails[] | undefined => { + const fileHash = props.fileHashes[fileName]; + if (!fileHash) return; + return props.markConfig[hexToNpub(usersPubkey)][fileHash]; + } + const { files } = props; + return ( + + {Object.entries(files) + .filter(([name]) => !!getMarkConfigDetails(name)) + .map(([name, file], i) => ( + + ))} + + ) + +} + +export default PdfView; \ No newline at end of file diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index e914172..215ca83 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -350,9 +350,17 @@ export const CreatePage = () => { if (!markConfig[drawnField.counterpart]) markConfig[drawnField.counterpart] = {} if (!markConfig[drawnField.counterpart][fileHash]) markConfig[drawnField.counterpart][fileHash] = [] + console.log('drawn field: ', drawnField); + markConfig[drawnField.counterpart][fileHash].push({ markType: drawnField.type, - markLocation: `P:${pageIndex};X:${drawnField.left};Y:${drawnField.top}` + markLocation: { + page: pageIndex, + top: drawnField.top, + left: drawnField.left, + height: drawnField.height, + width: drawnField.width, + } }) }) }) @@ -510,6 +518,8 @@ export const CreatePage = () => { const viewers = users.filter((user) => user.role === UserRole.viewer) const markConfig = createMarkConfig(fileHashes) + console.log('mark config: ', markConfig) + const content: CreateSignatureEventContent = { signers: signers.map((signer) => hexToNpub(signer.pubkey)), viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)), @@ -519,6 +529,8 @@ export const CreatePage = () => { title } + console.log('content: ', content) + setLoadingSpinnerDesc('Signing nostr event for create signature') const createSignature = await signEventForMetaFile( diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 5204ea7..c7f2866 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -22,7 +22,7 @@ import { generateKeysFile, getHash, hexToNpub, - isOnline, + isOnline, loadZip, now, npubToHex, parseJson, @@ -33,6 +33,10 @@ import { } from '../../utils' import { DisplayMeta } from './internal/displayMeta' import styles from './style.module.scss' +import { PdfFile } from '../../types/drawing.ts' +import { toFile, toPdfFile } from '../../utils/pdf.ts' +import PdfView from '../../components/PDFView' +import { MarkConfig } from '../../types/mark.ts' enum SignedStatus { Fully_Signed, User_Is_Next_Signer, @@ -58,7 +62,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('') @@ -70,6 +74,7 @@ export const SignPage = () => { const [signers, setSigners] = useState<`npub1${string}`[]>([]) const [viewers, setViewers] = useState<`npub1${string}`[]>([]) + const [markConfig, setMarkConfig] = useState(null) const [creatorFileHashes, setCreatorFileHashes] = useState<{ [key: string]: string }>({}) @@ -178,6 +183,9 @@ export const SignPage = () => { setViewers(createSignatureContent.viewers) setCreatorFileHashes(createSignatureContent.fileHashes) setSubmittedBy(createSignatureEvent.pubkey) + setMarkConfig(createSignatureContent.markConfig); + + console.log('createSignatureContent', createSignatureContent) setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[]) } @@ -262,16 +270,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) @@ -285,7 +290,7 @@ export const SignPage = () => { ) if (arrayBuffer) { - files[fileName] = arrayBuffer + files[fileName] = await convertToPdfFile(arrayBuffer, fileName); const hash = await getHash(arrayBuffer) if (hash) { @@ -296,10 +301,17 @@ export const SignPage = () => { } } + console.log('processed files: ', files); + setFiles(files) setCurrentFileHashes(fileHashes) } + const convertToPdfFile = async (arrayBuffer: ArrayBuffer, fileName: string) => { + const file = toFile(arrayBuffer, fileName); + return toPdfFile(file); + } + const parseKeysJson = async (zip: JSZip) => { const keysFileContent = await readContentOfZipEntry( zip, @@ -323,11 +335,7 @@ export const SignPage = () => { 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) @@ -398,32 +406,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) { @@ -434,6 +437,8 @@ export const SignPage = () => { } } + console.log('processed files: ', files); + setFiles(files) setCurrentFileHashes(fileHashes) @@ -916,6 +921,17 @@ export const SignPage = () => { )} )} + {markConfig && ()} + +
+ + +
+ ) diff --git a/src/pages/sign/internal/displayMeta.tsx b/src/pages/sign/internal/displayMeta.tsx index 730347e..ab32c31 100644 --- a/src/pages/sign/internal/displayMeta.tsx +++ b/src/pages/sign/internal/displayMeta.tsx @@ -34,10 +34,11 @@ import { UserComponent } from '../../../components/username' import { MetadataController } from '../../../controllers' import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils' import styles from '../style.module.scss' +import { PdfFile } from '../../../types/drawing.ts' type DisplayMetaProps = { meta: Meta - files: { [filename: string]: ArrayBuffer } + files: { [filename: string]: PdfFile } submittedBy: string signers: `npub1${string}`[] viewers: `npub1${string}`[] @@ -143,7 +144,7 @@ export const DisplayMeta = ({ }, [users, submittedBy]) const downloadFile = async (filename: string) => { - const arrayBuffer = files[filename] + const arrayBuffer = await files[filename].file.arrayBuffer() if (!arrayBuffer) return const blob = new Blob([arrayBuffer]) diff --git a/src/pages/sign/style.module.scss b/src/pages/sign/style.module.scss index 283f3d8..d3b1086 100644 --- a/src/pages/sign/style.module.scss +++ b/src/pages/sign/style.module.scss @@ -47,4 +47,36 @@ @extend .user; } } + + .fixedBottomForm { + position: fixed; + bottom: 0; + width: 50%; + border-top: 1px solid #ccc; + box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); + padding: 10px 20px; + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + } + + .fixedBottomForm input[type="text"] { + width: 80%; + padding: 10px; + font-size: 16px; + border: 1px solid #ccc; + border-radius: 4px; + } + + .fixedBottomForm button { + background-color: #3f3d56; + color: white; + border: none; + padding: 10px 20px; + font-size: 16px; + margin-left: 10px; + border-radius: 4px; + cursor: pointer; + } } diff --git a/src/types/mark.ts b/src/types/mark.ts index d3c85c7..57e1512 100644 --- a/src/types/mark.ts +++ b/src/types/mark.ts @@ -37,7 +37,15 @@ export interface MarkConfigDetails { /** * Coordinates in format: X:10;Y:50 */ - markLocation: string; + markLocation: MarkLocation; +} + +export interface MarkLocation { + top: number; + left: number; + height: number; + width: number; + page: number; } // Creator Meta Object Example diff --git a/src/types/zip.ts b/src/types/zip.ts index a2ef3e7..b4c97ac 100644 --- a/src/types/zip.ts +++ b/src/types/zip.ts @@ -1,4 +1,4 @@ -export interface OutputByType { + export interface OutputByType { base64: string string: string text: string @@ -10,4 +10,17 @@ export interface OutputByType { nodebuffer: Buffer } +interface InputByType { + base64: string; + string: string; + text: string; + binarystring: string; + array: number[]; + uint8array: Uint8Array; + arraybuffer: ArrayBuffer; + blob: Blob; + stream: NodeJS.ReadableStream; +} + export type OutputType = keyof OutputByType +export type InputFileFormat = InputByType[keyof InputByType] | Promise; \ No newline at end of file diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index a73f39f..341289a 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -20,6 +20,8 @@ const toPdfFiles = async (selectedFiles: File[]): Promise => { .map(toPdfFile)); } +const inPx = (coordinate: number): string => `${coordinate}px`; + const isPdf = (file: File) => file.type.toLowerCase().includes('pdf'); /** @@ -74,5 +76,6 @@ const pdfToImages = async (data: any): Promise => { export { toFile, toPdfFile, - toPdfFiles + toPdfFiles, + inPx } \ No newline at end of file diff --git a/src/utils/zip.ts b/src/utils/zip.ts index 71bc556..c289cbe 100644 --- a/src/utils/zip.ts +++ b/src/utils/zip.ts @@ -1,6 +1,6 @@ import JSZip from 'jszip' import { toast } from 'react-toastify' -import { OutputByType, OutputType } from '../types' +import { InputFileFormat, OutputByType, OutputType } from '../types' /** * Read the content of a file within a zip archive. @@ -9,7 +9,7 @@ import { OutputByType, OutputType } from '../types' * @param outputType The type of output to return (e.g., 'string', 'arraybuffer', 'uint8array', etc.). * @returns A Promise resolving to the content of the file, or null if an error occurs. */ -export const readContentOfZipEntry = async ( +const readContentOfZipEntry = async ( zip: JSZip, filePath: string, outputType: T @@ -35,3 +35,20 @@ export const readContentOfZipEntry = async ( // Return the file content or null if an error occurred return fileContent } + +const loadZip = async (data: InputFileFormat): Promise => { + try { + return await JSZip.loadAsync(data); + } catch (err: any) { + console.log('err in loading zip file :>> ', err) + toast.error(err.message || 'An error occurred in loading zip file.') + return null; + } +} + +export { + readContentOfZipEntry, + loadZip +} + + From 296b135c064ef877faa951bce06e4d3b6928b4cb Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 18 Jul 2024 15:38:07 +0300 Subject: [PATCH 04/17] feat(pdf-marking): updates mark type and adds pdf-view components --- .../DrawPDFFields/style.module.scss | 8 +- src/components/PDFView/PdfItem.tsx | 16 +- src/components/PDFView/PdfMarkItem.tsx | 20 +- src/components/PDFView/PdfPageItem.tsx | 40 +++- src/components/PDFView/index.tsx | 32 +-- src/components/PDFView/style.module.scss | 16 ++ src/pages/create/index.tsx | 39 ++-- src/pages/sign/MarkFormField.tsx | 32 +++ src/pages/sign/index.tsx | 211 +++++++++++++----- src/pages/sign/style.module.scss | 5 +- src/types/core.ts | 4 +- src/types/mark.ts | 28 ++- 12 files changed, 318 insertions(+), 133 deletions(-) create mode 100644 src/components/PDFView/style.module.scss create mode 100644 src/pages/sign/MarkFormField.tsx diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 4793f44..8f14fd1 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -59,14 +59,18 @@ .drawingRectangle { position: absolute; border: 1px solid #01aaad; - width: 40px; - height: 40px; + //width: 40px; + //height: 40px; z-index: 50; background-color: #01aaad4b; cursor: pointer; display: flex; justify-content: center; + &.nonEditable { + cursor: default; + } + .resizeHandle { position: absolute; right: -5px; diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx index 7ffd000..d463ea5 100644 --- a/src/components/PDFView/PdfItem.tsx +++ b/src/components/PDFView/PdfItem.tsx @@ -1,25 +1,25 @@ import { PdfFile } from '../../types/drawing.ts' -import { MarkConfigDetails} from '../../types/mark.ts' +import { Mark, MarkConfigDetails } from '../../types/mark.ts' import PdfPageItem from './PdfPageItem.tsx'; interface PdfItemProps { pdfFile: PdfFile - markConfigDetails: MarkConfigDetails[] + marks: Mark[] + handleMarkClick: (id: number) => void } -const PdfItem = ({ pdfFile, markConfigDetails }: PdfItemProps) => { - const filterMarkConfigDetails = (i: number) => { - return markConfigDetails.filter( - (details) => details.markLocation.page === i); +const PdfItem = ({ pdfFile, marks, handleMarkClick }: PdfItemProps) => { + const filterByPage = (marks: Mark[], page: number): Mark[] => { + return marks.filter((mark) => mark.location.page === page); } return ( pdfFile.pages.map((page, i) => { - console.log('page: ', page); return ( ) })) diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx index a170c45..60774e0 100644 --- a/src/components/PDFView/PdfMarkItem.tsx +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -1,20 +1,24 @@ -import { MarkLocation } from '../../types/mark.ts' +import { Mark, MarkLocation } from '../../types/mark.ts' import styles from '../DrawPDFFields/style.module.scss' import { inPx } from '../../utils/pdf.ts' interface PdfMarkItemProps { - markLocation: MarkLocation + mark: Mark + handleMarkClick: (id: number) => void + isEditable: boolean } -const PdfMarkItem = ({ markLocation }: PdfMarkItemProps) => { +const PdfMarkItem = ({ mark, handleMarkClick, isEditable }: PdfMarkItemProps) => { + const handleClick = () => isEditable && handleMarkClick(mark.id); return (
) diff --git a/src/components/PDFView/PdfPageItem.tsx b/src/components/PDFView/PdfPageItem.tsx index 5176fe9..051ac44 100644 --- a/src/components/PDFView/PdfPageItem.tsx +++ b/src/components/PDFView/PdfPageItem.tsx @@ -1,27 +1,45 @@ import styles from '../DrawPDFFields/style.module.scss' import { PdfPage } from '../../types/drawing.ts' -import { MarkConfigDetails, MarkLocation } from '../../types/mark.ts' +import { Mark, MarkConfigDetails } from '../../types/mark.ts' import PdfMarkItem from './PdfMarkItem.tsx' -import { useState } from 'react'; +import { useSelector } from 'react-redux' +import { State } from '../../store/rootReducer.ts' +import { hexToNpub } from '../../utils' interface PdfPageProps { page: PdfPage - markConfigDetails: MarkConfigDetails[] + marks: Mark[] + handleMarkClick: (id: number) => void } -const PdfPageItem = ({ page, markConfigDetails }: PdfPageProps) => { - const [currentMark, setCurrentMark] = useState(null); - +const PdfPageItem = ({ page, marks, handleMarkClick }: PdfPageProps) => { + const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) + const isEditable = (mark: Mark): boolean => { + if (!usersPubkey) return false; + return mark.npub === hexToNpub(usersPubkey); + } return (
- - {markConfigDetails.map((detail, i) => ( - + + { + marks.map((mark, i) => ( + ))}
) diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx index 6c6113c..1f72275 100644 --- a/src/components/PDFView/index.tsx +++ b/src/components/PDFView/index.tsx @@ -1,41 +1,29 @@ import { PdfFile } from '../../types/drawing.ts' import { Box } from '@mui/material' import PdfItem from './PdfItem.tsx' -import { MarkConfig, MarkConfigDetails } from '../../types/mark.ts' -import { State } from '../../store/rootReducer' -import { useSelector } from 'react-redux'; -import { hexToNpub, npubToHex } from '../../utils' +import { Mark, MarkConfigDetails } from '../../types/mark.ts' interface PdfViewProps { files: { [filename: string]: PdfFile }, fileHashes: { [key: string]: string | null }, - markConfig: MarkConfig, + marks: Mark[], + handleMarkClick: (id: number) => void } -const PdfView = (props: PdfViewProps) => { - console.log('current file hashes: ', props.fileHashes) - const usersPubkey = useSelector((state: State) => state.auth.usersPubkey); - if (!usersPubkey) return; - console.log(props.markConfig[hexToNpub(usersPubkey)]); - - console.log('users pubkey: ', usersPubkey); - console.log('mark config: ', props.markConfig); - - const getMarkConfigDetails = (fileName: string): MarkConfigDetails[] | undefined => { - const fileHash = props.fileHashes[fileName]; - if (!fileHash) return; - return props.markConfig[hexToNpub(usersPubkey)][fileHash]; +const PdfView = ({ files, fileHashes, marks, handleMarkClick }: PdfViewProps) => { + const filterByFile = (marks: Mark[], fileHash: string): Mark[] => { + return marks.filter((mark) => mark.pdfFileHash === fileHash); } - const { files } = props; return ( - + {Object.entries(files) - .filter(([name]) => !!getMarkConfigDetails(name)) .map(([name, file], i) => ( + marks={filterByFile(marks, fileHashes[name] ?? "")} + handleMarkClick={handleMarkClick} + /> ))} ) 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 215ca83..f509fa6 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -62,6 +62,8 @@ import { import styles from './style.module.scss' import { PdfFile } from '../../types/drawing' import { DrawPDFFields } from '../../components/DrawPDFFields' +import { Mark } from '../../types/mark.ts' +import { v4 as uuidv4 } from 'uuid'; export const CreatePage = () => { const navigate = useNavigate() @@ -339,34 +341,29 @@ export const CreatePage = () => { return fileHashes } - const createMarkConfig = (fileHashes: { [key: string]: string }) => { - const 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] = [] - - console.log('drawn field: ', drawnField); - - markConfig[drawnField.counterpart][fileHash].push({ - markType: drawnField.type, - markLocation: { - page: pageIndex, + const createMarkConfig = (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 diff --git a/src/pages/sign/MarkFormField.tsx b/src/pages/sign/MarkFormField.tsx new file mode 100644 index 0000000..2e63f38 --- /dev/null +++ b/src/pages/sign/MarkFormField.tsx @@ -0,0 +1,32 @@ +import { CurrentUserMark, Mark } from '../../types/mark.ts' +import styles from './style.module.scss' +import { Box, Button, TextField } from '@mui/material' + +interface MarkFormFieldProps { + handleSubmit: (event: any) => void + handleChange: (event: any) => void + currentMark: CurrentUserMark + currentMarkValue: string +} + +const MarkFormField = (props: MarkFormFieldProps) => { + const { handleSubmit, handleChange, currentMark, currentMarkValue } = props; + const getSubmitButton = () => currentMark.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 c7f2866..e8caa7c 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -1,8 +1,8 @@ -import { Box, Button, Typography } from '@mui/material' +import { Box, Button, FormControl, InputLabel, TextField, Typography } from '@mui/material' import axios from 'axios' import saveAs from 'file-saver' import JSZip from 'jszip' -import _ from 'lodash' +import _, { set } from 'lodash' import { MuiFileInput } from 'mui-file-input' import { Event, verifyEvent } from 'nostr-tools' import { useEffect, useState } from 'react' @@ -36,7 +36,8 @@ import styles from './style.module.scss' import { PdfFile } from '../../types/drawing.ts' import { toFile, toPdfFile } from '../../utils/pdf.ts' import PdfView from '../../components/PDFView' -import { MarkConfig } from '../../types/mark.ts' +import { CurrentUserMark, Mark, MarkConfig, MarkConfigDetails, User } from '../../types/mark.ts' +import MarkFormField from './MarkFormField.tsx' enum SignedStatus { Fully_Signed, User_Is_Next_Signer, @@ -74,7 +75,7 @@ export const SignPage = () => { const [signers, setSigners] = useState<`npub1${string}`[]>([]) const [viewers, setViewers] = useState<`npub1${string}`[]>([]) - const [markConfig, setMarkConfig] = useState(null) + const [marks, setMarks] = useState([]) const [creatorFileHashes, setCreatorFileHashes] = useState<{ [key: string]: string }>({}) @@ -93,6 +94,11 @@ export const SignPage = () => { const [authUrl, setAuthUrl] = useState() const nostrController = NostrController.getInstance() + const [currentUserMark, setCurrentUserMark] = useState(null); + const [currentUserMarks, setCurrentUserMarks] = useState([]); + const [currentMarkValue, setCurrentMarkValue] = useState(''); + const [isMarksCompleted, setIsMarksCompleted] = useState(false); + const [isLastUserMark, setIsLastUserMark] = useState(false); useEffect(() => { if (signers.length > 0) { @@ -183,9 +189,25 @@ export const SignPage = () => { setViewers(createSignatureContent.viewers) setCreatorFileHashes(createSignatureContent.fileHashes) setSubmittedBy(createSignatureEvent.pubkey) - setMarkConfig(createSignatureContent.markConfig); + setMarks(createSignatureContent.markConfig); - console.log('createSignatureContent', createSignatureContent) + console.log('createSignatureContent markConfig', createSignatureContent); + if (usersPubkey) { + console.log('this runs behind users pubkey'); + const curMarks = getCurrentUserMarks(createSignatureContent.markConfig, usersPubkey) + if (curMarks.length === 0) { + setIsMarksCompleted(true) + } else { + const nextMark = findNextIncompleteMark(curMarks) + if (!nextMark) { + setIsMarksCompleted(true) + } else { + setCurrentUserMark(nextMark) + setIsMarksCompleted(false) + } + setCurrentUserMarks(curMarks) + } + } setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[]) } @@ -514,6 +536,57 @@ export const SignPage = () => { ) } + const handleMarkClick = (id: number) => { + const nextMark = currentUserMarks.find(mark => mark.mark.id === id) + setCurrentUserMark(nextMark!) + setCurrentMarkValue(nextMark?.mark.value || "") + } + + const getMarkConfigPerUser = (markConfig: MarkConfig) => { + if (!usersPubkey) return; + return markConfig[hexToNpub(usersPubkey)]; + } + + const handleChange = (event: any) => setCurrentMarkValue(event.target.value); + + const handleSubmit = (event: any) => { + event.preventDefault(); + if (!currentMarkValue || !currentUserMark) return; + + const curMark = { + ...currentUserMark.mark, + value: currentMarkValue + }; + + const indexToUpdate = marks.findIndex(mark => mark.id === curMark.id); + + const updatedMarks: Mark[] = [ + ...marks.slice(0, indexToUpdate), + curMark, + ...marks.slice(indexToUpdate + 1) + ]; + + setMarks(updatedMarks) + setCurrentMarkValue("") + + const updatedCurUserMarks = getCurrentUserMarks(updatedMarks, usersPubkey!) + console.log('updatedCurUserMarks: ', updatedCurUserMarks) + setCurrentUserMarks(updatedCurUserMarks) + const nextMark = findNextIncompleteMark(updatedCurUserMarks) + console.log('next mark: ', nextMark) + if (!nextMark) { + setCurrentUserMark(null) + setIsMarksCompleted(true) + } else { + setCurrentUserMark(nextMark) + setIsMarksCompleted(false) + } + } + + const findNextIncompleteMark = (usersMarks: CurrentUserMark[]): CurrentUserMark | undefined => { + return usersMarks.find(mark => !mark.isCompleted); + } + // Update the meta signatures const updateMetaSignatures = (meta: Meta, signedEvent: SignedEvent): Meta => { const metaCopy = _.cloneDeep(meta) @@ -735,6 +808,25 @@ export const SignPage = () => { navigate(appPublicRoutes.verify) } + const getCurrentUserMarks = (marks: Mark[], pubkey: string): CurrentUserMark[] => { + console.log('marks: ', marks); + + const filteredMarks = marks + .filter(mark => mark.npub === hexToNpub(pubkey)) + const currentMarks = filteredMarks + .map((mark, index, arr) => { + return { + mark, + isLast: isLast(index, arr), + isCompleted: !!mark.value + } + }) + console.log('current marks: ', currentMarks) + return currentMarks; + } + + const isLast = (index: number, arr: any[]) => (index === (arr.length -1)) + const handleExportSigit = async () => { if (Object.entries(files).length === 0 || !meta) return @@ -852,9 +944,12 @@ export const SignPage = () => { ) } + if (isLoading) { + return + } + return ( <> - {isLoading && } {displayInput && ( <> @@ -881,56 +976,68 @@ export const SignPage = () => { )} - {submittedBy && Object.entries(files).length > 0 && meta && ( - <> - + {/*{submittedBy && Object.entries(files).length > 0 && meta && (*/} + {/* <>*/} + {/* */} - {signedStatus === SignedStatus.Fully_Signed && ( - - - - )} + {/* {signedStatus === SignedStatus.Fully_Signed && (*/} + {/* */} + {/* */} + {/* */} + {/* )}*/} - {signedStatus === SignedStatus.User_Is_Next_Signer && ( - - - - )} + {/* {signedStatus === SignedStatus.User_Is_Next_Signer && (*/} + {/* */} + {/* */} + {/* */} + {/* )}*/} - {isSignerOrCreator && ( - - - - )} - - )} - {markConfig && (*/} + {/* */} + {/* */} + {/* )}*/} + {/* */} + {/*)}*/} + { + !isMarksCompleted && marks.length > 0 && ( + )} + marks={marks} + fileHashes={currentFileHashes} + handleMarkClick={handleMarkClick} + /> + ) + } + + { + !isMarksCompleted && currentUserMark !== null && + } + + { isMarksCompleted &&

Ready to Sign!

} -
- - -
diff --git a/src/pages/sign/style.module.scss b/src/pages/sign/style.module.scss index d3b1086..aa3377b 100644 --- a/src/pages/sign/style.module.scss +++ b/src/pages/sign/style.module.scss @@ -51,7 +51,10 @@ .fixedBottomForm { position: fixed; bottom: 0; - width: 50%; + left: 50%; + transform: translateX(-50%); + width: 100%; + max-width: 500px; border-top: 1px solid #ccc; box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); padding: 10px 20px; diff --git a/src/types/core.ts b/src/types/core.ts index f4fb8b4..99a8cfa 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -1,4 +1,4 @@ -import { MarkConfig } from "./mark" +import { Mark } from './mark' import { Keys } from '../store/auth/types' export enum UserRole { @@ -23,7 +23,7 @@ export interface CreateSignatureEventContent { signers: `npub1${string}`[] viewers: `npub1${string}`[] fileHashes: { [key: string]: string } - markConfig: MarkConfig + markConfig: Mark[] title: string zipUrl: string } diff --git a/src/types/mark.ts b/src/types/mark.ts index 57e1512..3bc38f8 100644 --- a/src/types/mark.ts +++ b/src/types/mark.ts @@ -1,10 +1,25 @@ import { MarkType } from "./drawing"; +// export interface Mark { +// /** +// * @key png (pdf page) file hash +// */ +// [key: string]: MarkConfigDetails[] +// } + +export interface CurrentUserMark { + mark: Mark + isLast: boolean + isCompleted: boolean +} + export interface Mark { - /** - * @key png (pdf page) file hash - */ - [key: string]: MarkConfigDetails[] + id: number; + npub: string; + pdfFileHash: string; + type: MarkType; + location: MarkLocation; + value?: string; } export interface MarkConfig { @@ -33,11 +48,12 @@ export interface MarkValue { } export interface MarkConfigDetails { - markType: MarkType; + type: MarkType; /** * Coordinates in format: X:10;Y:50 */ - markLocation: MarkLocation; + location: MarkLocation; + value?: MarkValue } export interface MarkLocation { From 4a932ffe03cd4adad33abfdc7355335e4501038f Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 23 Jul 2024 12:22:53 +0300 Subject: [PATCH 05/17] feat(pdf-marking): binds text to marks and saves with signatures --- .../DrawPDFFields/style.module.scss | 2 + src/components/PDFView/PdfItem.tsx | 8 +- src/components/PDFView/PdfMarkItem.tsx | 13 +- src/components/PDFView/PdfPageItem.tsx | 8 +- src/components/PDFView/index.tsx | 8 +- src/pages/sign/MarkFormField.tsx | 3 +- src/pages/sign/const.ts | 5 + src/pages/sign/index.tsx | 144 ++++++++++-------- src/pages/sign/style.module.scss | 3 +- src/types/core.ts | 1 + 10 files changed, 122 insertions(+), 73 deletions(-) create mode 100644 src/pages/sign/const.ts diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 8f14fd1..b57aa65 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -66,9 +66,11 @@ cursor: pointer; display: flex; justify-content: center; + align-items: center; &.nonEditable { cursor: default; + visibility: hidden; } .resizeHandle { diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx index d463ea5..76c1b8b 100644 --- a/src/components/PDFView/PdfItem.tsx +++ b/src/components/PDFView/PdfItem.tsx @@ -1,14 +1,16 @@ import { PdfFile } from '../../types/drawing.ts' -import { Mark, MarkConfigDetails } from '../../types/mark.ts' +import { CurrentUserMark, Mark, MarkConfigDetails } from '../../types/mark.ts' import PdfPageItem from './PdfPageItem.tsx'; interface PdfItemProps { pdfFile: PdfFile marks: Mark[] handleMarkClick: (id: number) => void + currentMarkValue: string + currentUserMark: CurrentUserMark | null } -const PdfItem = ({ pdfFile, marks, handleMarkClick }: PdfItemProps) => { +const PdfItem = ({ pdfFile, marks, handleMarkClick, currentMarkValue, currentUserMark }: PdfItemProps) => { const filterByPage = (marks: Mark[], page: number): Mark[] => { return marks.filter((mark) => mark.location.page === page); } @@ -20,6 +22,8 @@ const PdfItem = ({ pdfFile, marks, handleMarkClick }: PdfItemProps) => { key={i} marks={filterByPage(marks, i)} handleMarkClick={handleMarkClick} + currentMarkValue={currentMarkValue} + currentUserMark={currentUserMark} /> ) })) diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx index 60774e0..bc265bf 100644 --- a/src/components/PDFView/PdfMarkItem.tsx +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -1,4 +1,4 @@ -import { Mark, MarkLocation } from '../../types/mark.ts' +import { CurrentUserMark, Mark, MarkLocation } from '../../types/mark.ts' import styles from '../DrawPDFFields/style.module.scss' import { inPx } from '../../utils/pdf.ts' @@ -6,10 +6,17 @@ interface PdfMarkItemProps { mark: Mark handleMarkClick: (id: number) => void isEditable: boolean + currentMarkValue: string + currentUserMark: CurrentUserMark | null } -const PdfMarkItem = ({ mark, handleMarkClick, isEditable }: PdfMarkItemProps) => { +const PdfMarkItem = ({ mark, handleMarkClick, isEditable, currentMarkValue, currentUserMark }: PdfMarkItemProps) => { const handleClick = () => isEditable && handleMarkClick(mark.id); + const getMarkValue = () => ( + currentUserMark?.mark.id === mark.id + ? currentMarkValue + : mark.value + ) return (
width: inPx(mark.location.width), height: inPx(mark.location.height) }} - /> + >{getMarkValue()}
) } diff --git a/src/components/PDFView/PdfPageItem.tsx b/src/components/PDFView/PdfPageItem.tsx index 051ac44..78a4ad1 100644 --- a/src/components/PDFView/PdfPageItem.tsx +++ b/src/components/PDFView/PdfPageItem.tsx @@ -1,6 +1,6 @@ import styles from '../DrawPDFFields/style.module.scss' import { PdfPage } from '../../types/drawing.ts' -import { Mark, MarkConfigDetails } from '../../types/mark.ts' +import { CurrentUserMark, Mark, MarkConfigDetails } from '../../types/mark.ts' import PdfMarkItem from './PdfMarkItem.tsx' import { useSelector } from 'react-redux' import { State } from '../../store/rootReducer.ts' @@ -9,9 +9,11 @@ interface PdfPageProps { page: PdfPage marks: Mark[] handleMarkClick: (id: number) => void + currentMarkValue: string + currentUserMark: CurrentUserMark | null } -const PdfPageItem = ({ page, marks, handleMarkClick }: PdfPageProps) => { +const PdfPageItem = ({ page, marks, handleMarkClick, currentMarkValue, currentUserMark }: PdfPageProps) => { const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const isEditable = (mark: Mark): boolean => { if (!usersPubkey) return false; @@ -39,6 +41,8 @@ const PdfPageItem = ({ page, marks, handleMarkClick }: PdfPageProps) => { mark={mark} isEditable={isEditable(mark)} handleMarkClick={handleMarkClick} + currentMarkValue={currentMarkValue} + currentUserMark={currentUserMark} /> ))}
diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx index 1f72275..6942b48 100644 --- a/src/components/PDFView/index.tsx +++ b/src/components/PDFView/index.tsx @@ -1,16 +1,18 @@ import { PdfFile } from '../../types/drawing.ts' import { Box } from '@mui/material' import PdfItem from './PdfItem.tsx' -import { Mark, MarkConfigDetails } from '../../types/mark.ts' +import { CurrentUserMark, Mark, MarkConfigDetails } from '../../types/mark.ts' interface PdfViewProps { files: { [filename: string]: PdfFile }, fileHashes: { [key: string]: string | null }, marks: Mark[], handleMarkClick: (id: number) => void + currentMarkValue: string + currentUserMark: CurrentUserMark | null } -const PdfView = ({ files, fileHashes, marks, handleMarkClick }: PdfViewProps) => { +const PdfView = ({ files, fileHashes, marks, handleMarkClick, currentMarkValue, currentUserMark }: PdfViewProps) => { const filterByFile = (marks: Mark[], fileHash: string): Mark[] => { return marks.filter((mark) => mark.pdfFileHash === fileHash); } @@ -23,6 +25,8 @@ const PdfView = ({ files, fileHashes, marks, handleMarkClick }: PdfViewProps) => key={i} marks={filterByFile(marks, fileHashes[name] ?? "")} handleMarkClick={handleMarkClick} + currentMarkValue={currentMarkValue} + currentUserMark={currentUserMark} /> ))} diff --git a/src/pages/sign/MarkFormField.tsx b/src/pages/sign/MarkFormField.tsx index 2e63f38..b45cbe8 100644 --- a/src/pages/sign/MarkFormField.tsx +++ b/src/pages/sign/MarkFormField.tsx @@ -1,6 +1,7 @@ import { CurrentUserMark, Mark } from '../../types/mark.ts' import styles from './style.module.scss' import { Box, Button, TextField } from '@mui/material' +import { MarkTypeTranslation } from './const.ts' interface MarkFormFieldProps { handleSubmit: (event: any) => void @@ -17,7 +18,7 @@ const MarkFormField = (props: MarkFormFieldProps) => { diff --git a/src/pages/sign/const.ts b/src/pages/sign/const.ts new file mode 100644 index 0000000..4a25c2b --- /dev/null +++ b/src/pages/sign/const.ts @@ -0,0 +1,5 @@ +import { MarkType } from '../../types/drawing.ts' + +export const MarkTypeTranslation: { [key: string]: string } = { + [MarkType.FULLNAME.valueOf()]: "Full Name" +} \ No newline at end of file diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index e8caa7c..1215862 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -13,7 +13,7 @@ import { LoadingSpinner } from '../../components/LoadingSpinner' import { NostrController } from '../../controllers' import { appPublicRoutes } from '../../routes' import { State } from '../../store/rootReducer' -import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types' +import { CreateSignatureEventContent, Meta, SignedEvent, UserRole } from '../../types' import { decryptArrayBuffer, encryptArrayBuffer, @@ -239,6 +239,7 @@ export const SignPage = () => { .then((res) => { handleArrayBufferFromBlossom(res.data, encryptionKey) setMeta(metaInNavState) + console.log('meta in nav state: ', metaInNavState) }) .catch((err) => { console.error(`error occurred in getting file from ${zipUrl}`, err) @@ -490,6 +491,8 @@ export const SignPage = () => { } ) + console.log('parsed meta: ', parsedMetaJson) + setMeta(parsedMetaJson) } @@ -514,7 +517,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) @@ -528,14 +534,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); + } + const handleMarkClick = (id: number) => { const nextMark = currentUserMarks.find(mark => mark.mark.id === id) setCurrentUserMark(nextMark!) @@ -595,6 +606,7 @@ export const SignPage = () => { [hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2) } metaCopy.modifiedAt = now() + console.log('meta copy: ', metaCopy); return metaCopy } @@ -719,6 +731,7 @@ export const SignPage = () => { await Promise.all(promises) .then(() => { toast.success('Notifications sent successfully') + console.log('meta: ', meta); setMeta(meta) }) .catch(() => { @@ -948,6 +961,34 @@ export const SignPage = () => { return } + if (!isMarksCompleted) { + return ( + <> + + { + marks.length > 0 && ( + )} + { + currentUserMark !== null && ( + + )} + + + + )} + return ( <> @@ -976,67 +1017,46 @@ export const SignPage = () => { )} - {/*{submittedBy && Object.entries(files).length > 0 && meta && (*/} - {/* <>*/} - {/* */} - - {/* {signedStatus === SignedStatus.Fully_Signed && (*/} - {/* */} - {/* */} - {/* */} - {/* )}*/} - - {/* {signedStatus === SignedStatus.User_Is_Next_Signer && (*/} - {/* */} - {/* */} - {/* */} - {/* )}*/} - - {/* {isSignerOrCreator && (*/} - {/* */} - {/* */} - {/* */} - {/* )}*/} - {/* */} - {/*)}*/} - { - !isMarksCompleted && marks.length > 0 && ( - 0 && meta && ( + <> + - ) - } - { - !isMarksCompleted && currentUserMark !== null && - } + {signedStatus === SignedStatus.Fully_Signed && ( + + + + )} - { isMarksCompleted &&

Ready to Sign!

} + {signedStatus === SignedStatus.User_Is_Next_Signer && ( + + + + )} + + {isSignerOrCreator && ( + + + + )} + + )}
diff --git a/src/pages/sign/style.module.scss b/src/pages/sign/style.module.scss index aa3377b..9f380f3 100644 --- a/src/pages/sign/style.module.scss +++ b/src/pages/sign/style.module.scss @@ -55,13 +55,14 @@ transform: translateX(-50%); width: 100%; max-width: 500px; + height: 100px; border-top: 1px solid #ccc; box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); padding: 10px 20px; display: flex; justify-content: center; align-items: center; - z-index: 1000; + //z-index: 200; } .fixedBottomForm input[type="text"] { diff --git a/src/types/core.ts b/src/types/core.ts index 99a8cfa..99c6925 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -30,6 +30,7 @@ export interface CreateSignatureEventContent { export interface SignedEventContent { prevSig: string + markConfig: Mark[] } export interface Sigit { From cb9a443fb18d5c562fe73400bb7aeedb4abf3f7e Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 25 Jul 2024 17:51:31 +0300 Subject: [PATCH 06/17] feat(pdf-marking): implements png to pdf conversion and ability to download full sigits after signing --- package.json | 1 + src/pages/sign/index.tsx | 60 +++---------- src/pages/verify/index.tsx | 104 +++++++++++++++++++++- src/types/core.ts | 2 +- src/utils/misc.ts | 10 ++- src/utils/pdf.ts | 178 +++++++++++++++++++++++++++++++++++-- src/utils/sign.ts | 33 +++++++ 7 files changed, 326 insertions(+), 62 deletions(-) create mode 100644 src/utils/sign.ts diff --git a/package.json b/package.json index fcaf0cd..5114c49 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,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/pages/sign/index.tsx b/src/pages/sign/index.tsx index 1215862..14228c3 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -1,8 +1,8 @@ -import { Box, Button, FormControl, InputLabel, TextField, Typography } from '@mui/material' +import { Box, Button, Typography } from '@mui/material' import axios from 'axios' import saveAs from 'file-saver' import JSZip from 'jszip' -import _, { set } from 'lodash' +import _ from 'lodash' import { MuiFileInput } from 'mui-file-input' import { Event, verifyEvent } from 'nostr-tools' import { useEffect, useState } from 'react' @@ -13,7 +13,7 @@ import { LoadingSpinner } from '../../components/LoadingSpinner' import { NostrController } from '../../controllers' import { appPublicRoutes } from '../../routes' import { State } from '../../store/rootReducer' -import { CreateSignatureEventContent, Meta, SignedEvent, UserRole } from '../../types' +import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types' import { decryptArrayBuffer, encryptArrayBuffer, @@ -34,10 +34,11 @@ import { import { DisplayMeta } from './internal/displayMeta' import styles from './style.module.scss' import { PdfFile } from '../../types/drawing.ts' -import { toFile, toPdfFile } from '../../utils/pdf.ts' +import { convertToPdfFile } from '../../utils/pdf.ts' import PdfView from '../../components/PDFView' -import { CurrentUserMark, Mark, MarkConfig, MarkConfigDetails, User } from '../../types/mark.ts' +import { CurrentUserMark, Mark, MarkConfig } from '../../types/mark.ts' import MarkFormField from './MarkFormField.tsx' +import { getLastSignersSig } from '../../utils/sign.ts' enum SignedStatus { Fully_Signed, User_Is_Next_Signer, @@ -98,7 +99,6 @@ export const SignPage = () => { const [currentUserMarks, setCurrentUserMarks] = useState([]); const [currentMarkValue, setCurrentMarkValue] = useState(''); const [isMarksCompleted, setIsMarksCompleted] = useState(false); - const [isLastUserMark, setIsLastUserMark] = useState(false); useEffect(() => { if (signers.length > 0) { @@ -330,11 +330,6 @@ export const SignPage = () => { setCurrentFileHashes(fileHashes) } - const convertToPdfFile = async (arrayBuffer: ArrayBuffer, fileName: string) => { - const file = toFile(arrayBuffer, fileName); - return toPdfFile(file); - } - const parseKeysJson = async (zip: JSZip) => { const keysFileContent = await readContentOfZipEntry( zip, @@ -491,8 +486,6 @@ export const SignPage = () => { } ) - console.log('parsed meta: ', parsedMetaJson) - setMeta(parsedMetaJson) } @@ -616,7 +609,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`, { @@ -763,7 +756,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( @@ -813,7 +808,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) @@ -878,7 +873,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`) } @@ -915,37 +910,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 (