diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index 0d7407f..92dc01d 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -1,5 +1,5 @@ import { Meta } from '../../types' -import { SigitCardDisplayInfo, SigitStatus } from '../../utils' +import { SigitCardDisplayInfo, SigitStatus, SignStatus } from '../../utils' import { Link } from 'react-router-dom' import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils' import { appPublicRoutes, appPrivateRoutes } from '../../routes' @@ -13,7 +13,6 @@ import { faFile } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { UserAvatar } from '../UserAvatar' import { UserAvatarGroup } from '../UserAvatarGroup' import styles from './style.module.scss' @@ -34,7 +33,8 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { submittedBy, signers, signedStatus, - fileExtensions + fileExtensions, + isValid } = parsedMeta const { signersStatus } = useSigitMeta(meta) @@ -62,6 +62,7 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { const profile = profiles[submittedBy] return ( { disableInteractive > - + ) diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx index fc7d43d..3681cfd 100644 --- a/src/components/UsersDetails.tsx/index.tsx +++ b/src/components/UsersDetails.tsx/index.tsx @@ -9,7 +9,6 @@ import { shorten, SignStatus } from '../../utils' -import { UserAvatar } from '../UserAvatar' import { useSigitMeta } from '../../hooks/useSigitMeta' import { UserAvatarGroup } from '../UserAvatarGroup' @@ -44,7 +43,8 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { createdAt, completedAt, parsedSignatureEvents, - signedStatus + signedStatus, + isValid } = useSigitMeta(meta) const { usersPubkey } = useSelector((state: State) => state.auth) const profiles = useSigitProfiles([ @@ -56,7 +56,7 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { typeof usersPubkey !== 'undefined' && signers.includes(hexToNpub(usersPubkey)) - const ext = extractFileExtensions(Object.keys(fileHashes)) + const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes)) return submittedBy ? (
@@ -68,6 +68,7 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { const profile = profiles[submittedBy] return ( { disableInteractive > - + ) @@ -196,14 +201,14 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { {signedStatus} - {ext.length > 0 ? ( + {extensions.length > 0 ? ( - {ext.length > 1 ? ( + {!isSame ? ( <> Multiple File Types ) : ( - getExtensionIconLabel(ext[0]) + getExtensionIconLabel(extensions[0]) )} ) : ( diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index a393824..fea5154 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -1,5 +1,10 @@ import { useEffect, useState } from 'react' -import { CreateSignatureEventContent, Meta, SignedEventContent } from '../types' +import { + CreateSignatureEventContent, + DocSignatureEvent, + Meta, + SignedEventContent +} from '../types' import { Mark } from '../types/mark' import { fromUnixTimestamp, @@ -38,7 +43,9 @@ export interface FlatMeta encryptionKey: string | null // Parsed Document Signatures - parsedSignatureEvents: { [signer: `npub1${string}`]: Event } + parsedSignatureEvents: { + [signer: `npub1${string}`]: DocSignatureEvent + } // Calculated completion time completedAt?: number @@ -74,7 +81,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { const [zipUrl, setZipUrl] = useState('') const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{ - [signer: `npub1${string}`]: Event + [signer: `npub1${string}`]: DocSignatureEvent }>({}) const [completedAt, setCompletedAt] = useState() @@ -141,7 +148,10 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { } // Temp. map to hold events and signers - const parsedSignatureEventsMap = new Map<`npub1${string}`, Event>() + const parsedSignatureEventsMap = new Map< + `npub1${string}`, + DocSignatureEvent + >() const signerStatusMap = new Map<`npub1${string}`, SignStatus>() const getPrevSignerSig = (npub: `npub1${string}`) => { @@ -183,9 +193,12 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { if (isValidSignature) { // get the signature of prev signer from the content of current signers signedEvent const prevSignersSig = getPrevSignerSig(npub) - try { const obj: SignedEventContent = JSON.parse(event.content) + parsedSignatureEventsMap.set(npub, { + ...event, + parsedContent: obj + }) if ( obj.prevSig && prevSignersSig && diff --git a/src/layouts/StickySideColumns.module.scss b/src/layouts/StickySideColumns.module.scss index fd99964..7690822 100644 --- a/src/layouts/StickySideColumns.module.scss +++ b/src/layouts/StickySideColumns.module.scss @@ -23,7 +23,11 @@ grid-gap: 15px; } .content { - max-width: 550px; - width: 550px; + padding: 10px; + border: 10px solid $overlay-background-color; + border-radius: 4px; + + max-width: 590px; + width: 590px; margin: 0 auto; } diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 69f7f0b..ceba0eb 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -1,12 +1,16 @@ -import { Box, Button, Tooltip, Typography, useTheme } from '@mui/material' +import { Box, Button, Divider, Tooltip, Typography } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' import { Event, verifyEvent } from 'nostr-tools' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { NostrController } from '../../controllers' -import { CreateSignatureEventContent, Meta } from '../../types' +import { + CreateSignatureEventContent, + DocSignatureEvent, + Meta +} from '../../types' import { decryptArrayBuffer, extractMarksFromSignedMeta, @@ -16,10 +20,10 @@ import { parseJson, readContentOfZipEntry, signEventForMetaFile, - shorten + shorten, + getCurrentUserFiles } from '../../utils' import styles from './style.module.scss' -import { Cancel, CheckCircle } from '@mui/icons-material' import { useLocation } from 'react-router-dom' import axios from 'axios' import { PdfFile } from '../../types/drawing.ts' @@ -27,7 +31,8 @@ import { addMarks, convertToPdfBlob, convertToPdfFile, - groupMarksByPage + groupMarksByFileNamePage, + inPx } from '../../utils/pdf.ts' import { State } from '../../store/rootReducer.ts' import { useSelector } from 'react-redux' @@ -40,13 +45,101 @@ import { UsersDetails } from '../../components/UsersDetails.tsx/index.tsx' import { UserAvatar } from '../../components/UserAvatar/index.tsx' import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx' import { TooltipChild } from '../../components/TooltipChild.tsx' +import FileList from '../../components/FileList' +import { CurrentUserFile } from '../../types/file.ts' +import { Mark } from '../../types/mark.ts' + +interface PdfViewProps { + files: CurrentUserFile[] + currentFile: CurrentUserFile | null + parsedSignatureEvents: { + [signer: `npub1${string}`]: DocSignatureEvent + } +} + +const SlimPdfView = ({ + files, + currentFile, + parsedSignatureEvents +}: PdfViewProps) => { + const pdfRefs = useRef<(HTMLDivElement | null)[]>([]) + useEffect(() => { + if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { + pdfRefs.current[currentFile.id]?.scrollIntoView({ + behavior: 'smooth', + block: 'end' + }) + } + }, [currentFile]) + return ( +
+ {files.map((currentUserFile, i) => { + const { hash, filename, pdfFile, id } = currentUserFile + const signatureEvents = Object.keys(parsedSignatureEvents) + if (!hash) return + return ( + <> +
(pdfRefs.current[id] = el)} + key={filename} + className={styles.fileWrapper} + > + {pdfFile.pages.map((page, i) => { + const marks: Mark[] = [] + + signatureEvents.forEach((e) => { + const m = parsedSignatureEvents[ + e as `npub1${string}` + ].parsedContent?.marks.filter( + (m) => m.pdfFileHash == hash && m.location.page == i + ) + if (m) { + marks.push(...m) + } + }) + return ( +
+ + {marks.map((m) => { + return ( +
+ {m.value} +
+ ) + })} +
+ ) + })} +
+ + {i < files.length - 1 && ( + + File Separator + + )} + + ) + })} +
+ ) +} export const VerifyPage = () => { - const theme = useTheme() - const textColor = theme.palette.getContrastText( - theme.palette.background.paper - ) - const location = useLocation() /** * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json @@ -54,8 +147,15 @@ export const VerifyPage = () => { */ const { uploadedZip, meta } = location.state || {} - const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } = - useSigitMeta(meta) + const { + submittedBy, + zipUrl, + encryptionKey, + signers, + viewers, + fileHashes, + parsedSignatureEvents + } = useSigitMeta(meta) const profiles = useSigitProfiles([ ...(submittedBy ? [submittedBy] : []), @@ -70,8 +170,23 @@ export const VerifyPage = () => { const [currentFileHashes, setCurrentFileHashes] = useState<{ [key: string]: string | null - }>(fileHashes) + }>({}) const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) + const [currentFile, setCurrentFile] = useState(null) + const [signatureFileHashes, setSignatureFileHashes] = useState<{ + [key: string]: string + }>(fileHashes) + + useEffect(() => { + setSignatureFileHashes(fileHashes) + }, [fileHashes]) + + useEffect(() => { + if (Object.entries(files).length > 0) { + const tmp = getCurrentUserFiles(files, fileHashes, signatureFileHashes) + setCurrentFile(tmp[0]) + } + }, [signatureFileHashes, fileHashes, files]) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const nostrController = NostrController.getInstance() @@ -203,7 +318,6 @@ export const VerifyPage = () => { } } - console.log('fileHashes :>> ', fileHashes) setCurrentFileHashes(fileHashes) setLoadingSpinnerDesc('Parsing meta.json') @@ -307,10 +421,10 @@ export const VerifyPage = () => { zip.file('meta.json', stringifiedMeta) const marks = extractMarksFromSignedMeta(updatedMeta) - const marksByPage = groupMarksByPage(marks) + const marksByPage = groupMarksByFileNamePage(marks) for (const [fileName, pdf] of Object.entries(files)) { - const pages = await addMarks(pdf.file, marksByPage) + const pages = await addMarks(pdf.file, marksByPage[fileName]) const blob = await convertToPdfBlob(pages) zip.file(`files/${fileName}`, blob) } @@ -414,51 +528,33 @@ export const VerifyPage = () => { - - {Object.entries(currentFileHashes).map( - ([filename, hash], index) => { - const isValidHash = fileHashes[filename] === hash - - return ( - - - {filename} - - {isValidHash && ( - - - - )} - {!isValidHash && ( - - - - )} - - ) - } - )} - + {currentFile !== null && ( + + )} {displayExportedBy()} - - - } right={} - /> + > + + )} diff --git a/src/pages/verify/style.module.scss b/src/pages/verify/style.module.scss index ea6408f..a0dec2f 100644 --- a/src/pages/verify/style.module.scss +++ b/src/pages/verify/style.module.scss @@ -50,3 +50,36 @@ } } } + +.view { + width: 550px; + max-width: 550px; + + display: flex; + flex-direction: column; + gap: 25px; +} + +.imageWrapper { + position: relative; + + img { + width: 100%; + display: block; + } +} + +.fileWrapper { + display: flex; + flex-direction: column; + gap: 15px; +} + +.mark { + position: absolute; + border: 1px dotted black; + + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/types/core.ts b/src/types/core.ts index 609837f..8583d4a 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -1,5 +1,6 @@ import { Mark } from './mark' import { Keys } from '../store/auth/types' +import { Event } from 'nostr-tools' export enum UserRole { signer = 'Signer', @@ -44,3 +45,7 @@ export interface UserAppData { keyPair?: Keys // this key pair is used for blossom requests authentication blossomUrls: string[] // array for storing Urls for the files that stores all the sigits and processedGiftWraps on blossom } + +export interface DocSignatureEvent extends Event { + parsedContent?: SignedEventContent +} diff --git a/src/utils/file.ts b/src/utils/file.ts index 94308d5..401d3c4 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,6 +1,6 @@ import { Meta } from '../types' import { extractMarksFromSignedMeta } from './mark.ts' -import { addMarks, convertToPdfBlob, groupMarksByPage } from './pdf.ts' +import { addMarks, convertToPdfBlob, groupMarksByFileNamePage } from './pdf.ts' import JSZip from 'jszip' import { PdfFile } from '../types/drawing.ts' @@ -10,12 +10,12 @@ const getZipWithFiles = async ( ): Promise => { const zip = new JSZip() const marks = extractMarksFromSignedMeta(meta) - const marksByPage = groupMarksByPage(marks) + const marksByFileNamePage = groupMarksByFileNamePage(marks) for (const [fileName, pdf] of Object.entries(files)) { - const pages = await addMarks(pdf.file, marksByPage) + const pages = await addMarks(pdf.file, marksByFileNamePage[fileName]) const blob = await convertToPdfBlob(pages) - zip.file(`/files/${fileName}`, blob) + zip.file(`files/${fileName}`, blob) } return zip diff --git a/src/utils/mark.ts b/src/utils/mark.ts index 18cc3e8..f0cd47e 100644 --- a/src/utils/mark.ts +++ b/src/utils/mark.ts @@ -119,7 +119,11 @@ const getUpdatedMark = ( return { ...selectedMark, currentValue: selectedMarkValue, - isCompleted: !!selectedMarkValue + isCompleted: !!selectedMarkValue, + mark: { + ...selectedMark.mark, + value: selectedMarkValue + } } } diff --git a/src/utils/meta.ts b/src/utils/meta.ts index 4915f19..276f049 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -1,6 +1,6 @@ import { CreateSignatureEventContent, Meta } from '../types' import { fromUnixTimestamp, parseJson } from '.' -import { Event } from 'nostr-tools' +import { Event, verifyEvent } from 'nostr-tools' import { toast } from 'react-toastify' export enum SignStatus { @@ -75,6 +75,7 @@ export interface SigitCardDisplayInfo { signers: `npub1${string}`[] fileExtensions: string[] signedStatus: SigitStatus + isValid: boolean } /** @@ -128,12 +129,15 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { const sigitInfo: SigitCardDisplayInfo = { signers: [], fileExtensions: [], - signedStatus: SigitStatus.Partial + signedStatus: SigitStatus.Partial, + isValid: false } try { const createSignatureEvent = await parseNostrEvent(meta.createSignature) + sigitInfo.isValid = verifyEvent(createSignatureEvent) + // created_at in nostr events are stored in seconds sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at) @@ -142,7 +146,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { ) const files = Object.keys(createSignatureContent.fileHashes) - const extensions = extractFileExtensions(files) + const { extensions } = extractFileExtensions(files) const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] const isCompletelySigned = createSignatureContent.signers.every((signer) => @@ -169,6 +173,10 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { } } +/** + * @param fileNames - List of filenames to check + * @returns List of extensions and if all are same + */ export const extractFileExtensions = (fileNames: string[]) => { const extensions = fileNames.reduce((result: string[], file: string) => { const extension = file.split('.').pop() @@ -178,5 +186,7 @@ export const extractFileExtensions = (fileNames: string[]) => { return result }, []) - return extensions + const isSame = extensions.every((ext) => ext === extensions[0]) + + return { extensions, isSame } } diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index 622a259..ce2f132 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -71,14 +71,19 @@ const isPdf = (file: File) => file.type.toLowerCase().includes('pdf') /** * Reads the pdf file binaries */ -const readPdf = (file: File): Promise => { +const readPdf = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader() - reader.onload = (e: any) => { - const data = e.target.result - - resolve(data) + reader.onload = (e) => { + const data = e.target?.result + // Make sure we only resolve for string or ArrayBuffer type + // They are accepted by PDFJS.getDocument function + if (data && typeof data !== 'undefined') { + resolve(data) + } else { + reject(new Error('File is null or undefined')) + } } reader.onerror = (err) => { @@ -94,7 +99,7 @@ const readPdf = (file: File): Promise => { * Converts pdf to the images * @param data pdf file bytes */ -const pdfToImages = async (data: any): Promise => { +const pdfToImages = async (data: string | ArrayBuffer): Promise => { const images: string[] = [] const pdf = await PDFJS.getDocument(data).promise const canvas = document.createElement('canvas') @@ -142,7 +147,8 @@ const addMarks = async ( canvas.width = viewport.width await page.render({ canvasContext: context!, viewport: viewport }).promise - marksPerPage[i]?.forEach((mark) => draw(mark, context!)) + if (marksPerPage && Object.hasOwn(marksPerPage, i)) + marksPerPage[i]?.forEach((mark) => draw(mark, context!)) images.push(canvas.toDataURL()) } @@ -230,11 +236,11 @@ const convertToPdfFile = async ( * @function scaleMark scales remaining marks in line with SCALE * @function byPage groups remaining Marks by their page marks.location.page */ -const groupMarksByPage = (marks: Mark[]) => { +const groupMarksByFileNamePage = (marks: Mark[]) => { return marks .filter(hasValue) .map(scaleMark) - .reduce<{ [key: number]: Mark[] }>(byPage, {}) + .reduce<{ [filename: string]: { [page: number]: Mark[] } }>(byPage, {}) } /** @@ -245,10 +251,21 @@ const groupMarksByPage = (marks: Mark[]) => { * @param obj - accumulator in the reducer callback * @param mark - current value, i.e. Mark being examined */ -const byPage = (obj: { [key: number]: Mark[] }, mark: Mark) => { - const key = mark.location.page - const curGroup = obj[key] ?? [] - return { ...obj, [key]: [...curGroup, mark] } +const byPage = ( + obj: { [filename: string]: { [page: number]: Mark[] } }, + mark: Mark +) => { + const filename = mark.fileName + const pageNumber = mark.location.page + const pages = obj[filename] ?? {} + const marks = pages[pageNumber] ?? [] + return { + ...obj, + [filename]: { + ...pages, + [pageNumber]: [...marks, mark] + } + } } export { @@ -259,5 +276,5 @@ export { convertToPdfFile, addMarks, convertToPdfBlob, - groupMarksByPage + groupMarksByFileNamePage }