Compare commits

...

8 Commits

Author SHA1 Message Date
963c8cdd20 fix(marks): add file grouping for marks, fix read pdf types
Some checks failed
Open PR on Staging / audit_and_check (pull_request) Failing after 32s
2024-08-16 12:01:41 +02:00
cb10b38e94 fix(sigit): add to submittedBy avatar badge for verified sigit creation 2024-08-15 17:48:05 +02:00
5662153cc9 fix(verify-page): add mark styling 2024-08-15 17:34:11 +02:00
5803875bbc revert: "feat(pdf-marking): adds file validity check"
Refs: ed7acd6cb4
2024-08-15 17:03:29 +02:00
e5d2ba87e8 fix(verify-page): export (download) files now includes files
The issue was noticed on the windows machine, removing forward slash made it work
2024-08-15 16:46:21 +02:00
609fa4f513 fix(marks): assign selectedMarkValue to currentValue and mark.value 2024-08-15 16:46:21 +02:00
a280e0a10f fix(verify-page): parse and show mark values 2024-08-15 16:46:21 +02:00
ced6cc086a feat(verify-page): add files view and content images 2024-08-15 16:46:21 +02:00
16 changed files with 283 additions and 140 deletions

View File

@ -1,5 +1,5 @@
import { Meta } from '../../types' import { Meta } from '../../types'
import { SigitCardDisplayInfo, SigitStatus } from '../../utils' import { SigitCardDisplayInfo, SigitStatus, SignStatus } from '../../utils'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils' import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils'
import { appPublicRoutes, appPrivateRoutes } from '../../routes' import { appPublicRoutes, appPrivateRoutes } from '../../routes'
@ -13,7 +13,6 @@ import {
faFile faFile
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { UserAvatar } from '../UserAvatar'
import { UserAvatarGroup } from '../UserAvatarGroup' import { UserAvatarGroup } from '../UserAvatarGroup'
import styles from './style.module.scss' import styles from './style.module.scss'
@ -34,7 +33,8 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
submittedBy, submittedBy,
signers, signers,
signedStatus, signedStatus,
fileExtensions fileExtensions,
isValid
} = parsedMeta } = parsedMeta
const { signersStatus } = useSigitMeta(meta) const { signersStatus } = useSigitMeta(meta)
@ -62,6 +62,7 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
const profile = profiles[submittedBy] const profile = profiles[submittedBy]
return ( return (
<Tooltip <Tooltip
key={submittedBy}
title={ title={
profile?.display_name || profile?.display_name ||
profile?.name || profile?.name ||
@ -72,7 +73,11 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
disableInteractive disableInteractive
> >
<TooltipChild> <TooltipChild>
<UserAvatar pubkey={submittedBy} image={profile?.picture} /> <DisplaySigner
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
profile={profile}
pubkey={submittedBy}
/>
</TooltipChild> </TooltipChild>
</Tooltip> </Tooltip>
) )

View File

@ -1,8 +1,6 @@
import { CurrentUserFile } from '../../types/file.ts' import { CurrentUserFile } from '../../types/file.ts'
import styles from './style.module.scss' import styles from './style.module.scss'
import { Button } from '@mui/material' import { Button } from '@mui/material'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheck } from '@fortawesome/free-solid-svg-icons'
interface FileListProps { interface FileListProps {
files: CurrentUserFile[] files: CurrentUserFile[]
@ -28,14 +26,8 @@ const FileList = ({
className={`${styles.fileItem} ${isActive(file) && styles.active}`} className={`${styles.fileItem} ${isActive(file) && styles.active}`}
onClick={() => setCurrentFile(file)} onClick={() => setCurrentFile(file)}
> >
<div className={styles.fileNumber}>{file.id}</div> <span className={styles.fileNumber}>{file.id}</span>
<div className={styles.fileInfo}> <span className={styles.fileName}>{file.filename}</span>
<div className={styles.fileName}>{file.filename}</div>
</div>
<div className={styles.fileVisual}>
{file.isHashValid && <FontAwesomeIcon icon={faCheck} />}
</div>
</li> </li>
))} ))}
</ul> </ul>

View File

@ -21,12 +21,6 @@ ul {
padding: 0; /* Removes default padding */ padding: 0; /* Removes default padding */
} }
li {
list-style-type: none; /* Removes the bullets */
margin: 0; /* Removes any default margin */
padding: 0; /* Removes any default padding */
}
.wrap { .wrap {
display: flex; display: flex;
@ -89,12 +83,6 @@ li {
color: white; color: white;
} }
.fileInfo {
flex-grow: 1;
font-size: 16px;
font-weight: 500;
}
.fileName { .fileName {
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
@ -109,15 +97,4 @@ li {
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 10px;
}
.fileVisual {
display: flex;
flex-shrink: 0;
flex-direction: column;
justify-content: center;
align-items: center;
height: 25px;
width: 25px;
} }

View File

@ -9,7 +9,6 @@ import {
shorten, shorten,
SignStatus SignStatus
} from '../../utils' } from '../../utils'
import { UserAvatar } from '../UserAvatar'
import { useSigitMeta } from '../../hooks/useSigitMeta' import { useSigitMeta } from '../../hooks/useSigitMeta'
import { UserAvatarGroup } from '../UserAvatarGroup' import { UserAvatarGroup } from '../UserAvatarGroup'
@ -44,7 +43,8 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
createdAt, createdAt,
completedAt, completedAt,
parsedSignatureEvents, parsedSignatureEvents,
signedStatus signedStatus,
isValid
} = useSigitMeta(meta) } = useSigitMeta(meta)
const { usersPubkey } = useSelector((state: State) => state.auth) const { usersPubkey } = useSelector((state: State) => state.auth)
const profiles = useSigitProfiles([ const profiles = useSigitProfiles([
@ -56,7 +56,7 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
typeof usersPubkey !== 'undefined' && typeof usersPubkey !== 'undefined' &&
signers.includes(hexToNpub(usersPubkey)) signers.includes(hexToNpub(usersPubkey))
const ext = extractFileExtensions(Object.keys(fileHashes)) const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
return submittedBy ? ( return submittedBy ? (
<div className={styles.container}> <div className={styles.container}>
@ -68,6 +68,7 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
const profile = profiles[submittedBy] const profile = profiles[submittedBy]
return ( return (
<Tooltip <Tooltip
key={submittedBy}
title={ title={
profile?.display_name || profile?.display_name ||
profile?.name || profile?.name ||
@ -78,7 +79,11 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
disableInteractive disableInteractive
> >
<TooltipChild> <TooltipChild>
<UserAvatar pubkey={submittedBy} image={profile?.picture} /> <DisplaySigner
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
profile={profile}
pubkey={submittedBy}
/>
</TooltipChild> </TooltipChild>
</Tooltip> </Tooltip>
) )
@ -196,14 +201,14 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
<span className={styles.detailsItem}> <span className={styles.detailsItem}>
<FontAwesomeIcon icon={faEye} /> {signedStatus} <FontAwesomeIcon icon={faEye} /> {signedStatus}
</span> </span>
{ext.length > 0 ? ( {extensions.length > 0 ? (
<span className={styles.detailsItem}> <span className={styles.detailsItem}>
{ext.length > 1 ? ( {!isSame ? (
<> <>
<FontAwesomeIcon icon={faFile} /> Multiple File Types <FontAwesomeIcon icon={faFile} /> Multiple File Types
</> </>
) : ( ) : (
getExtensionIconLabel(ext[0]) getExtensionIconLabel(extensions[0])
)} )}
</span> </span>
) : ( ) : (

View File

@ -1,5 +1,10 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { CreateSignatureEventContent, Meta, SignedEventContent } from '../types' import {
CreateSignatureEventContent,
DocSignatureEvent,
Meta,
SignedEventContent
} from '../types'
import { Mark } from '../types/mark' import { Mark } from '../types/mark'
import { import {
fromUnixTimestamp, fromUnixTimestamp,
@ -38,7 +43,9 @@ export interface FlatMeta
encryptionKey: string | null encryptionKey: string | null
// Parsed Document Signatures // Parsed Document Signatures
parsedSignatureEvents: { [signer: `npub1${string}`]: Event } parsedSignatureEvents: {
[signer: `npub1${string}`]: DocSignatureEvent
}
// Calculated completion time // Calculated completion time
completedAt?: number completedAt?: number
@ -74,7 +81,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
const [zipUrl, setZipUrl] = useState<string>('') const [zipUrl, setZipUrl] = useState<string>('')
const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{ const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{
[signer: `npub1${string}`]: Event [signer: `npub1${string}`]: DocSignatureEvent
}>({}) }>({})
const [completedAt, setCompletedAt] = useState<number>() const [completedAt, setCompletedAt] = useState<number>()
@ -141,7 +148,10 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
} }
// Temp. map to hold events and signers // 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 signerStatusMap = new Map<`npub1${string}`, SignStatus>()
const getPrevSignerSig = (npub: `npub1${string}`) => { const getPrevSignerSig = (npub: `npub1${string}`) => {
@ -183,9 +193,12 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
if (isValidSignature) { if (isValidSignature) {
// get the signature of prev signer from the content of current signers signedEvent // get the signature of prev signer from the content of current signers signedEvent
const prevSignersSig = getPrevSignerSig(npub) const prevSignersSig = getPrevSignerSig(npub)
try { try {
const obj: SignedEventContent = JSON.parse(event.content) const obj: SignedEventContent = JSON.parse(event.content)
parsedSignatureEventsMap.set(npub, {
...event,
parsedContent: obj
})
if ( if (
obj.prevSig && obj.prevSig &&
prevSignersSig && prevSignersSig &&

View File

@ -23,7 +23,11 @@
grid-gap: 15px; grid-gap: 15px;
} }
.content { .content {
max-width: 550px; padding: 10px;
width: 550px; border: 10px solid $overlay-background-color;
border-radius: 4px;
max-width: 590px;
width: 590px;
margin: 0 auto; margin: 0 auto;
} }

View File

@ -952,7 +952,7 @@ export const SignPage = () => {
return ( return (
<PdfMarking <PdfMarking
files={getCurrentUserFiles(files, currentFileHashes, creatorFileHashes)} files={getCurrentUserFiles(files, currentFileHashes)}
currentUserMarks={currentUserMarks} currentUserMarks={currentUserMarks}
setIsReadyToSign={setIsReadyToSign} setIsReadyToSign={setIsReadyToSign}
setCurrentUserMarks={setCurrentUserMarks} setCurrentUserMarks={setCurrentUserMarks}

View File

@ -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 JSZip from 'jszip'
import { MuiFileInput } from 'mui-file-input' import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools' import { Event, verifyEvent } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
import { NostrController } from '../../controllers' import { NostrController } from '../../controllers'
import { CreateSignatureEventContent, Meta } from '../../types' import {
CreateSignatureEventContent,
DocSignatureEvent,
Meta
} from '../../types'
import { import {
decryptArrayBuffer, decryptArrayBuffer,
extractMarksFromSignedMeta, extractMarksFromSignedMeta,
@ -16,10 +20,10 @@ import {
parseJson, parseJson,
readContentOfZipEntry, readContentOfZipEntry,
signEventForMetaFile, signEventForMetaFile,
shorten shorten,
getCurrentUserFiles
} from '../../utils' } from '../../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
import { Cancel, CheckCircle } from '@mui/icons-material'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import axios from 'axios' import axios from 'axios'
import { PdfFile } from '../../types/drawing.ts' import { PdfFile } from '../../types/drawing.ts'
@ -27,7 +31,8 @@ import {
addMarks, addMarks,
convertToPdfBlob, convertToPdfBlob,
convertToPdfFile, convertToPdfFile,
groupMarksByPage groupMarksByFileNamePage,
inPx
} from '../../utils/pdf.ts' } from '../../utils/pdf.ts'
import { State } from '../../store/rootReducer.ts' import { State } from '../../store/rootReducer.ts'
import { useSelector } from 'react-redux' 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 { UserAvatar } from '../../components/UserAvatar/index.tsx'
import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx' import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx'
import { TooltipChild } from '../../components/TooltipChild.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 (
<div className={styles.view}>
{files.map((currentUserFile, i) => {
const { hash, filename, pdfFile, id } = currentUserFile
const signatureEvents = Object.keys(parsedSignatureEvents)
if (!hash) return
return (
<>
<div
id={filename}
ref={(el) => (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 (
<div className={styles.imageWrapper} key={i}>
<img draggable="false" src={page.image} />
{marks.map((m) => {
return (
<div
className={styles.mark}
key={m.id}
style={{
left: inPx(m.location.left),
top: inPx(m.location.top),
width: inPx(m.location.width),
height: inPx(m.location.height)
}}
>
{m.value}
</div>
)
})}
</div>
)
})}
</div>
{i < files.length - 1 && (
<Divider
sx={{
fontSize: '12px',
color: 'rgba(0,0,0,0.15)'
}}
>
File Separator
</Divider>
)}
</>
)
})}
</div>
)
}
export const VerifyPage = () => { export const VerifyPage = () => {
const theme = useTheme()
const textColor = theme.palette.getContrastText(
theme.palette.background.paper
)
const location = useLocation() const location = useLocation()
/** /**
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json * 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 { uploadedZip, meta } = location.state || {}
const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } = const {
useSigitMeta(meta) submittedBy,
zipUrl,
encryptionKey,
signers,
viewers,
fileHashes,
parsedSignatureEvents
} = useSigitMeta(meta)
const profiles = useSigitProfiles([ const profiles = useSigitProfiles([
...(submittedBy ? [submittedBy] : []), ...(submittedBy ? [submittedBy] : []),
@ -72,6 +172,15 @@ export const VerifyPage = () => {
[key: string]: string | null [key: string]: string | null
}>(fileHashes) }>(fileHashes)
const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({})
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
useEffect(() => {
if (Object.entries(files).length > 0) {
const tmp = getCurrentUserFiles(files, fileHashes)
setCurrentFile(tmp[0])
}
}, [fileHashes, files])
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
@ -203,7 +312,6 @@ export const VerifyPage = () => {
} }
} }
console.log('fileHashes :>> ', fileHashes)
setCurrentFileHashes(fileHashes) setCurrentFileHashes(fileHashes)
setLoadingSpinnerDesc('Parsing meta.json') setLoadingSpinnerDesc('Parsing meta.json')
@ -307,12 +415,12 @@ export const VerifyPage = () => {
zip.file('meta.json', stringifiedMeta) zip.file('meta.json', stringifiedMeta)
const marks = extractMarksFromSignedMeta(updatedMeta) const marks = extractMarksFromSignedMeta(updatedMeta)
const marksByPage = groupMarksByPage(marks) const marksByPage = groupMarksByFileNamePage(marks)
for (const [fileName, pdf] of Object.entries(files)) { 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) const blob = await convertToPdfBlob(pages)
zip.file(`/files/${fileName}`, blob) zip.file(`files/${fileName}`, blob)
} }
const arrayBuffer = await zip const arrayBuffer = await zip
@ -414,51 +522,25 @@ export const VerifyPage = () => {
<StickySideColumns <StickySideColumns
left={ left={
<> <>
<Box className={styles.filesWrapper}> {currentFile !== null && (
{Object.entries(currentFileHashes).map( <FileList
([filename, hash], index) => { files={getCurrentUserFiles(files, currentFileHashes)}
const isValidHash = fileHashes[filename] === hash currentFile={currentFile}
setCurrentFile={setCurrentFile}
return ( handleDownload={handleExport}
<Box key={`file-${index}`} className={styles.file}>
<Typography
component="label"
sx={{
color: textColor,
flexGrow: 1
}}
>
{filename}
</Typography>
{isValidHash && (
<Tooltip title="File integrity check passed" arrow>
<CheckCircle
sx={{ color: theme.palette.success.light }}
/> />
</Tooltip>
)} )}
{!isValidHash && (
<Tooltip title="File integrity check failed" arrow>
<Cancel
sx={{ color: theme.palette.error.main }}
/>
</Tooltip>
)}
</Box>
)
}
)}
</Box>
{displayExportedBy()} {displayExportedBy()}
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExport} variant="contained">
Export Sigit
</Button>
</Box>
</> </>
} }
right={<UsersDetails meta={meta} />} right={<UsersDetails meta={meta} />}
>
<SlimPdfView
currentFile={currentFile}
files={getCurrentUserFiles(files, currentFileHashes)}
parsedSignatureEvents={parsedSignatureEvents}
/> />
</StickySideColumns>
)} )}
</Container> </Container>
</> </>

View File

@ -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;
}

View File

@ -1,5 +1,6 @@
import { Mark } from './mark' import { Mark } from './mark'
import { Keys } from '../store/auth/types' import { Keys } from '../store/auth/types'
import { Event } from 'nostr-tools'
export enum UserRole { export enum UserRole {
signer = 'Signer', signer = 'Signer',
@ -44,3 +45,7 @@ export interface UserAppData {
keyPair?: Keys // this key pair is used for blossom requests authentication 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 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
}

View File

@ -5,5 +5,4 @@ export interface CurrentUserFile {
pdfFile: PdfFile pdfFile: PdfFile
filename: string filename: string
hash?: string hash?: string
isHashValid: boolean
} }

View File

@ -1,6 +1,6 @@
import { Meta } from '../types' import { Meta } from '../types'
import { extractMarksFromSignedMeta } from './mark.ts' import { extractMarksFromSignedMeta } from './mark.ts'
import { addMarks, convertToPdfBlob, groupMarksByPage } from './pdf.ts' import { addMarks, convertToPdfBlob, groupMarksByFileNamePage } from './pdf.ts'
import JSZip from 'jszip' import JSZip from 'jszip'
import { PdfFile } from '../types/drawing.ts' import { PdfFile } from '../types/drawing.ts'
@ -10,12 +10,12 @@ const getZipWithFiles = async (
): Promise<JSZip> => { ): Promise<JSZip> => {
const zip = new JSZip() const zip = new JSZip()
const marks = extractMarksFromSignedMeta(meta) const marks = extractMarksFromSignedMeta(meta)
const marksByPage = groupMarksByPage(marks) const marksByFileNamePage = groupMarksByFileNamePage(marks)
for (const [fileName, pdf] of Object.entries(files)) { 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) const blob = await convertToPdfBlob(pages)
zip.file(`/files/${fileName}`, blob) zip.file(`files/${fileName}`, blob)
} }
return zip return zip

View File

@ -119,7 +119,11 @@ const getUpdatedMark = (
return { return {
...selectedMark, ...selectedMark,
currentValue: selectedMarkValue, currentValue: selectedMarkValue,
isCompleted: !!selectedMarkValue isCompleted: !!selectedMarkValue,
mark: {
...selectedMark.mark,
value: selectedMarkValue
}
} }
} }

View File

@ -1,6 +1,6 @@
import { CreateSignatureEventContent, Meta } from '../types' import { CreateSignatureEventContent, Meta } from '../types'
import { fromUnixTimestamp, parseJson } from '.' import { fromUnixTimestamp, parseJson } from '.'
import { Event } from 'nostr-tools' import { Event, verifyEvent } from 'nostr-tools'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
export enum SignStatus { export enum SignStatus {
@ -75,6 +75,7 @@ export interface SigitCardDisplayInfo {
signers: `npub1${string}`[] signers: `npub1${string}`[]
fileExtensions: string[] fileExtensions: string[]
signedStatus: SigitStatus signedStatus: SigitStatus
isValid: boolean
} }
/** /**
@ -128,12 +129,15 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
const sigitInfo: SigitCardDisplayInfo = { const sigitInfo: SigitCardDisplayInfo = {
signers: [], signers: [],
fileExtensions: [], fileExtensions: [],
signedStatus: SigitStatus.Partial signedStatus: SigitStatus.Partial,
isValid: false
} }
try { try {
const createSignatureEvent = await parseNostrEvent(meta.createSignature) const createSignatureEvent = await parseNostrEvent(meta.createSignature)
sigitInfo.isValid = verifyEvent(createSignatureEvent)
// created_at in nostr events are stored in seconds // created_at in nostr events are stored in seconds
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at) sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
@ -142,7 +146,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
) )
const files = Object.keys(createSignatureContent.fileHashes) const files = Object.keys(createSignatureContent.fileHashes)
const extensions = extractFileExtensions(files) const { extensions } = extractFileExtensions(files)
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
const isCompletelySigned = createSignatureContent.signers.every((signer) => 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[]) => { export const extractFileExtensions = (fileNames: string[]) => {
const extensions = fileNames.reduce((result: string[], file: string) => { const extensions = fileNames.reduce((result: string[], file: string) => {
const extension = file.split('.').pop() const extension = file.split('.').pop()
@ -178,5 +186,7 @@ export const extractFileExtensions = (fileNames: string[]) => {
return result return result
}, []) }, [])
return extensions const isSame = extensions.every((ext) => ext === extensions[0])
return { extensions, isSame }
} }

View File

@ -71,14 +71,19 @@ const isPdf = (file: File) => file.type.toLowerCase().includes('pdf')
/** /**
* Reads the pdf file binaries * Reads the pdf file binaries
*/ */
const readPdf = (file: File): Promise<string> => { const readPdf = (file: File): Promise<string | ArrayBuffer> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader() const reader = new FileReader()
reader.onload = (e: any) => { reader.onload = (e) => {
const data = e.target.result 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) resolve(data)
} else {
reject(new Error('File is null or undefined'))
}
} }
reader.onerror = (err) => { reader.onerror = (err) => {
@ -94,7 +99,7 @@ const readPdf = (file: File): Promise<string> => {
* Converts pdf to the images * Converts pdf to the images
* @param data pdf file bytes * @param data pdf file bytes
*/ */
const pdfToImages = async (data: any): Promise<PdfPage[]> => { const pdfToImages = async (data: string | ArrayBuffer): Promise<PdfPage[]> => {
const images: string[] = [] const images: string[] = []
const pdf = await PDFJS.getDocument(data).promise const pdf = await PDFJS.getDocument(data).promise
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
@ -142,6 +147,7 @@ const addMarks = async (
canvas.width = viewport.width canvas.width = viewport.width
await page.render({ canvasContext: context!, viewport: viewport }).promise await page.render({ canvasContext: context!, viewport: viewport }).promise
if (marksPerPage && Object.hasOwn(marksPerPage, i))
marksPerPage[i]?.forEach((mark) => draw(mark, context!)) marksPerPage[i]?.forEach((mark) => draw(mark, context!))
images.push(canvas.toDataURL()) images.push(canvas.toDataURL())
@ -230,11 +236,11 @@ const convertToPdfFile = async (
* @function scaleMark scales remaining marks in line with SCALE * @function scaleMark scales remaining marks in line with SCALE
* @function byPage groups remaining Marks by their page marks.location.page * @function byPage groups remaining Marks by their page marks.location.page
*/ */
const groupMarksByPage = (marks: Mark[]) => { const groupMarksByFileNamePage = (marks: Mark[]) => {
return marks return marks
.filter(hasValue) .filter(hasValue)
.map(scaleMark) .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 obj - accumulator in the reducer callback
* @param mark - current value, i.e. Mark being examined * @param mark - current value, i.e. Mark being examined
*/ */
const byPage = (obj: { [key: number]: Mark[] }, mark: Mark) => { const byPage = (
const key = mark.location.page obj: { [filename: string]: { [page: number]: Mark[] } },
const curGroup = obj[key] ?? [] mark: Mark
return { ...obj, [key]: [...curGroup, 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 { export {
@ -259,5 +276,5 @@ export {
convertToPdfFile, convertToPdfFile,
addMarks, addMarks,
convertToPdfBlob, convertToPdfBlob,
groupMarksByPage groupMarksByFileNamePage
} }

View File

@ -72,20 +72,17 @@ export const timeout = (ms: number = 60000) => {
* including its name, hash, and content * including its name, hash, and content
* @param files * @param files
* @param fileHashes * @param fileHashes
* @param creatorFileHashes
*/ */
export const getCurrentUserFiles = ( export const getCurrentUserFiles = (
files: { [filename: string]: PdfFile }, files: { [filename: string]: PdfFile },
fileHashes: { [key: string]: string | null }, fileHashes: { [key: string]: string | null }
creatorFileHashes: { [key: string]: string }
): CurrentUserFile[] => { ): CurrentUserFile[] => {
return Object.entries(files).map(([filename, pdfFile], index) => { return Object.entries(files).map(([filename, pdfFile], index) => {
return { return {
pdfFile, pdfFile,
filename, filename,
id: index + 1, id: index + 1,
...(!!fileHashes[filename] && { hash: fileHashes[filename]! }), ...(!!fileHashes[filename] && { hash: fileHashes[filename]! })
isHashValid: creatorFileHashes[filename] === fileHashes[filename]
} }
}) })
} }