diff --git a/src/App.scss b/src/App.scss index 6724890..1b5bc87 100644 --- a/src/App.scss +++ b/src/App.scss @@ -69,3 +69,61 @@ a { input { font-family: inherit; } + +// Shared styles for center content (Create, Sign, Verify) +.files-wrapper { + display: flex; + flex-direction: column; + gap: 25px; +} + +.file-wrapper { + display: flex; + flex-direction: column; + gap: 15px; + position: relative; + + // CSS, scroll position when scrolling to the files is adjusted by + // - first-child Header height, default body padding, and center content border (10px) and padding (10px) + // - others We don't include border and padding and scroll to the top of the image + &:first-child { + scroll-margin-top: $header-height + $body-vertical-padding + 20px; + } + &:not(:first-child) { + scroll-margin-top: $header-height + $body-vertical-padding; + } +} + +.image-wrapper { + position: relative; + -webkit-user-select: none; + user-select: none; + + overflow: hidden; /* Ensure no overflow */ + + > img { + display: block; + max-width: 100%; + max-height: 100%; + object-fit: contain; /* Ensure the image fits within the container */ + } +} + +[data-dev='true'] { + .image-wrapper { + // outline: 1px solid #ccc; /* Optional: for visual debugging */ + background-color: #e0f7fa; /* Optional: for visual debugging */ + } +} + +.extension-file-box { + border-radius: 4px; + background: rgba(255, 255, 255, 0.5); + height: 100px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: rgba(0, 0, 0, 0.25); + font-size: 14px; +} diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 3da227d..cc4d286 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -2,7 +2,6 @@ import { Close } from '@mui/icons-material' import { Box, CircularProgress, - Divider, FormControl, InputLabel, MenuItem, @@ -10,19 +9,15 @@ import { } from '@mui/material' import styles from './style.module.scss' import React, { useEffect, useState } from 'react' - import * as PDFJS from 'pdfjs-dist' import { ProfileMetadata, User, UserRole } from '../../types' -import { - PdfFile, - MouseState, - PdfPage, - DrawnField, - DrawTool -} from '../../types/drawing' +import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing' import { truncate } from 'lodash' -import { extractFileExtension, hexToNpub } from '../../utils' -import { toPdfFiles } from '../../utils/pdf.ts' +import { settleAllFullfilfedPromises, hexToNpub } from '../../utils' +import { getSigitFile, SigitFile } from '../../utils/file' +import { FileDivider } from '../FileDivider' +import { ExtensionFileBox } from '../ExtensionFileBox' + PDFJS.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url @@ -32,15 +27,15 @@ interface Props { selectedFiles: File[] users: User[] metadata: { [key: string]: ProfileMetadata } - onDrawFieldsChange: (pdfFiles: PdfFile[]) => void + onDrawFieldsChange: (sigitFiles: SigitFile[]) => void selectedTool?: DrawTool } export const DrawPDFFields = (props: Props) => { const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props - const [pdfFiles, setPdfFiles] = useState([]) - const [parsingPdf, setParsingPdf] = useState(false) + const [sigitFiles, setSigitFiles] = useState([]) + const [parsingPdf, setIsParsing] = useState(false) const [mouseState, setMouseState] = useState({ clicked: false @@ -49,42 +44,43 @@ export const DrawPDFFields = (props: Props) => { useEffect(() => { if (selectedFiles) { /** - * Reads the pdf binary files and converts it's pages to images - * creates the pdfFiles object and sets to a state + * Reads the binary files and converts to internal file type + * and sets to a state (adds images if it's a PDF) */ - const parsePdfPages = async () => { - const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles) + const parsePages = async () => { + const files = await settleAllFullfilfedPromises( + selectedFiles, + getSigitFile + ) - setPdfFiles(pdfFiles) + setSigitFiles(files) } - setParsingPdf(true) + setIsParsing(true) - parsePdfPages().finally(() => { - setParsingPdf(false) + parsePages().finally(() => { + setIsParsing(false) }) } }, [selectedFiles]) useEffect(() => { - if (pdfFiles) onDrawFieldsChange(pdfFiles) - }, [onDrawFieldsChange, pdfFiles]) + if (sigitFiles) onDrawFieldsChange(sigitFiles) + }, [onDrawFieldsChange, sigitFiles]) /** * Drawing events */ useEffect(() => { - // window.addEventListener('mousedown', onMouseDown); window.addEventListener('mouseup', onMouseUp) return () => { - // window.removeEventListener('mousedown', onMouseDown); window.removeEventListener('mouseup', onMouseUp) } }, []) const refreshPdfFiles = () => { - setPdfFiles([...pdfFiles]) + setSigitFiles([...sigitFiles]) } /** @@ -303,10 +299,10 @@ export const DrawPDFFields = (props: Props) => { ) => { event.stopPropagation() - pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice( - drawnFileIndex, - 1 - ) + const pages = sigitFiles[pdfFileIndex]?.pages + if (pages) { + pages[pdfPageIndex]?.drawnFields?.splice(drawnFileIndex, 1) + } } /** @@ -345,14 +341,17 @@ export const DrawPDFFields = (props: Props) => { /** * Renders the pdf pages and drawing elements */ - const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => { + const getPdfPages = (file: SigitFile, fileIndex: number) => { + // Early return if this is not a pdf + if (!file.isPdf) return null + return ( <> - {pdfFile.pages.map((page, pdfPageIndex: number) => { + {file.pages?.map((page, pageIndex: number) => { return (
{ @@ -393,8 +392,8 @@ export const DrawPDFFields = (props: Props) => { onMouseDown={(event) => { onRemoveHandleMouseDown( event, - pdfFileIndex, - pdfPageIndex, + fileIndex, + pageIndex, drawnFieldIndex ) }} @@ -469,38 +468,24 @@ export const DrawPDFFields = (props: Props) => { ) } - if (!pdfFiles.length) { + if (!sigitFiles.length) { return '' } return ( -
- {selectedFiles.map((file, i) => { +
+ {sigitFiles.map((file, i) => { const name = file.name - const extension = extractFileExtension(name) - const pdfFile = pdfFiles.find((pdf) => pdf.file.name === name) return ( -
- {pdfFile ? ( - getPdfPages(pdfFile, i) +
+ {file.isPdf ? ( + getPdfPages(file, i) ) : ( -
This is a {extension} file
+ )}
- {i < selectedFiles.length - 1 && ( - - File Separator - - )} + {i < selectedFiles.length - 1 && } ) })} diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 8c888ec..83844ce 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -8,17 +8,6 @@ } .pdfImageWrapper { - position: relative; - -webkit-user-select: none; - user-select: none; - - > img { - display: block; - max-width: 100%; - max-height: 100%; - object-fit: contain; /* Ensure the image fits within the container */ - } - &.drawing { cursor: crosshair; } @@ -90,17 +79,3 @@ padding: 5px 0; } } - -.fileWrapper { - display: flex; - flex-direction: column; - gap: 15px; - position: relative; - scroll-margin-top: $header-height + $body-vertical-padding; -} - -.view { - display: flex; - flex-direction: column; - gap: 25px; -} diff --git a/src/components/ExtensionFileBox.tsx b/src/components/ExtensionFileBox.tsx new file mode 100644 index 0000000..f36d38c --- /dev/null +++ b/src/components/ExtensionFileBox.tsx @@ -0,0 +1,6 @@ +interface ExtensionFileBoxProps { + extension: string +} +export const ExtensionFileBox = ({ extension }: ExtensionFileBoxProps) => ( +
This is a {extension} file
+) diff --git a/src/components/FileDivider.tsx b/src/components/FileDivider.tsx new file mode 100644 index 0000000..b66b8f4 --- /dev/null +++ b/src/components/FileDivider.tsx @@ -0,0 +1,12 @@ +import Divider from '@mui/material/Divider/Divider' + +export const FileDivider = () => ( + + File Separator + +) diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx index 53557a5..38fab88 100644 --- a/src/components/FileList/index.tsx +++ b/src/components/FileList/index.tsx @@ -24,19 +24,23 @@ const FileList = ({
    - {files.map((file: CurrentUserFile) => ( + {files.map((currentUserFile: CurrentUserFile) => (
  • setCurrentFile(file)} + key={currentUserFile.id} + className={`${styles.fileItem} ${isActive(currentUserFile) && styles.active}`} + onClick={() => setCurrentFile(currentUserFile)} > -
    {file.id}
    +
    {currentUserFile.id}
    -
    {file.filename}
    +
    + {currentUserFile.file.name} +
    - {file.isHashValid && } + {currentUserFile.isHashValid && ( + + )}
  • ))} diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx index c7f3b54..b6707d3 100644 --- a/src/components/PDFView/PdfItem.tsx +++ b/src/components/PDFView/PdfItem.tsx @@ -1,13 +1,13 @@ -import { PdfFile } from '../../types/drawing.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts' -import { extractFileExtension } from '../../utils/meta.ts' +import { SigitFile } from '../../utils/file.ts' +import { ExtensionFileBox } from '../ExtensionFileBox.tsx' import PdfPageItem from './PdfPageItem.tsx' interface PdfItemProps { currentUserMarks: CurrentUserMark[] handleMarkClick: (id: number) => void otherUserMarks: Mark[] - pdfFile: PdfFile | File + file: SigitFile selectedMark: CurrentUserMark | null selectedMarkValue: string } @@ -16,7 +16,7 @@ interface PdfItemProps { * Responsible for displaying pages of a single Pdf File. */ const PdfItem = ({ - pdfFile, + file, currentUserMarks, handleMarkClick, selectedMarkValue, @@ -32,8 +32,8 @@ const PdfItem = ({ const filterMarksByPage = (marks: Mark[], page: number): Mark[] => { return marks.filter((mark) => mark.location.page === page) } - if ('pages' in pdfFile) { - return pdfFile.pages.map((page, i) => { + if (file.isPdf) { + return file.pages?.map((page, i) => { return ( This is a {extension} file
+ return } } diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index 9fff924..dafe42b 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -10,7 +10,6 @@ import { import { EMPTY } from '../../utils/const.ts' import { Container } from '../Container' import signPageStyles from '../../pages/sign/style.module.scss' -import styles from './style.module.scss' import { CurrentUserFile } from '../../types/file.ts' import FileList from '../FileList' import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' @@ -134,21 +133,17 @@ const PdfMarking = (props: PdfMarkingProps) => { } right={meta !== null && } > -
- {currentUserMarks?.length > 0 && ( -
- -
- )} -
+ {currentUserMarks?.length > 0 && ( + + )} {selectedMark !== null && ( { if (selectedMark !== null && !!markRefs.current[selectedMark.id]) { markRefs.current[selectedMark.id]?.scrollIntoView({ - behavior: 'smooth', - block: 'end' + behavior: 'smooth' }) } }, [selectedMark]) const markRefs = useRef<(HTMLDivElement | null)[]>([]) return ( -
- +
+ {currentUserMarks.map((m, i) => (
(markRefs.current[m.id] = el)}> ([]) useEffect(() => { if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { - pdfRefs.current[currentFile.id]?.scrollIntoView({ - behavior: 'smooth', - block: 'end' - }) + pdfRefs.current[currentFile.id]?.scrollIntoView({ behavior: 'smooth' }) } }, [currentFile]) const filterByFile = ( @@ -49,30 +47,32 @@ const PdfView = ({ const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean => index !== files.length - 1 return ( - <> +
{files.map((currentUserFile, index, arr) => { - const { hash, pdfFile, id, filename } = currentUserFile + const { hash, file, id } = currentUserFile if (!hash) return return ( -
(pdfRefs.current[id] = el)} - key={index} - > - - {isNotLastPdfFile(index, arr) && File Separator} -
+ +
(pdfRefs.current[id] = el)} + > + +
+ {isNotLastPdfFile(index, arr) && } +
) })} - +
) } diff --git a/src/components/PDFView/style.module.scss b/src/components/PDFView/style.module.scss index 3a893d4..870057a 100644 --- a/src/components/PDFView/style.module.scss +++ b/src/components/PDFView/style.module.scss @@ -1,33 +1,7 @@ -.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 */ -} - .container { display: flex; width: 100%; flex-direction: column; - -} - -.pdfView { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - gap: 10px; } .otherUserMarksDisplay { diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx index 3681cfd..16ff440 100644 --- a/src/components/UsersDetails.tsx/index.tsx +++ b/src/components/UsersDetails.tsx/index.tsx @@ -1,7 +1,6 @@ import { Divider, Tooltip } from '@mui/material' import { useSigitProfiles } from '../../hooks/useSigitProfiles' import { - extractFileExtensions, formatTimestamp, fromUnixTimestamp, hexToNpub, @@ -28,6 +27,7 @@ import { State } from '../../store/rootReducer' import { TooltipChild } from '../TooltipChild' import { DisplaySigner } from '../DisplaySigner' import { Meta } from '../../types' +import { extractFileExtensions } from '../../utils/file' interface UsersDetailsProps { meta: Meta diff --git a/src/controllers/RelayController.ts b/src/controllers/RelayController.ts index 40945ad..df33b4b 100644 --- a/src/controllers/RelayController.ts +++ b/src/controllers/RelayController.ts @@ -1,5 +1,9 @@ import { Event, Filter, Relay } from 'nostr-tools' -import { normalizeWebSocketURL, timeout } from '../utils' +import { + settleAllFullfilfedPromises, + normalizeWebSocketURL, + timeout +} from '../utils' import { SIGIT_RELAY } from '../utils/const' /** @@ -105,24 +109,11 @@ export class RelayController { } // connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => - this.connectRelay(relayUrl) + const relays = await settleAllFullfilfedPromises( + relayUrls, + this.connectRelay ) - // Use Promise.allSettled to wait for all promises to settle - const results = await Promise.allSettled(relayPromises) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) - // Check if any relays are connected if (relays.length === 0) { throw new Error('No relay is connected to fetch events!') @@ -228,23 +219,10 @@ export class RelayController { } // connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => { - return this.connectRelay(relayUrl) - }) - - // Use Promise.allSettled to wait for all promises to settle - const results = await Promise.allSettled(relayPromises) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) + const relays = await settleAllFullfilfedPromises( + relayUrls, + this.connectRelay + ) // Check if any relays are connected if (relays.length === 0) { @@ -292,24 +270,11 @@ export class RelayController { } // connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => - this.connectRelay(relayUrl) + const relays = await settleAllFullfilfedPromises( + relayUrls, + this.connectRelay ) - // Use Promise.allSettled to wait for all promises to settle - const results = await Promise.allSettled(relayPromises) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) - // Check if any relays are connected if (relays.length === 0) { throw new Error('No relay is connected to publish event!') diff --git a/src/index.css b/src/index.css index b292cb2..7ee0eea 100644 --- a/src/index.css +++ b/src/index.css @@ -165,15 +165,3 @@ button:disabled { font-style: normal; font-display: swap; } - -.otherFile { - border-radius: 4px; - background: rgba(255, 255, 255, 0.5); - height: 100px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: rgba(0, 0, 0, 0.25); - font-size: 14px; -} diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 8a73012..2bcd063 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -51,7 +51,7 @@ import { import { Container } from '../../components/Container' import styles from './style.module.scss' import fileListStyles from '../../components/FileList/style.module.scss' -import { DrawTool, MarkType, PdfFile } from '../../types/drawing' +import { DrawTool, MarkType } from '../../types/drawing' import { DrawPDFFields } from '../../components/DrawPDFFields' import { Mark } from '../../types/mark.ts' import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' @@ -83,6 +83,7 @@ import { faTrash, faUpload } from '@fortawesome/free-solid-svg-icons' +import { SigitFile } from '../../utils/file.ts' export const CreatePage = () => { const navigate = useNavigate() @@ -125,7 +126,7 @@ export const CreatePage = () => { const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( {} ) - const [drawnPdfs, setDrawnPdfs] = useState([]) + const [drawnFiles, setDrawnFiles] = useState([]) const [selectedTool, setSelectedTool] = useState() const [toolbox] = useState([ @@ -507,26 +508,28 @@ export const CreatePage = () => { } const createMarks = (fileHashes: { [key: string]: string }): Mark[] => { - return drawnPdfs - .flatMap((drawnPdf) => { - const fileHash = fileHashes[drawnPdf.file.name] - return drawnPdf.pages.flatMap((page, index) => { - return page.drawnFields.map((drawnField) => { - return { - type: drawnField.type, - location: { - page: index, - top: drawnField.top, - left: drawnField.left, - height: drawnField.height, - width: drawnField.width - }, - npub: drawnField.counterpart, - pdfFileHash: fileHash, - fileName: drawnPdf.file.name - } - }) - }) + return drawnFiles + .flatMap((file) => { + const fileHash = fileHashes[file.name] + return ( + file.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, + fileName: file.name + } + }) + }) || [] + ) }) .map((mark, index) => { return { ...mark, id: index } @@ -846,8 +849,8 @@ export const CreatePage = () => { } } - const onDrawFieldsChange = (pdfFiles: PdfFile[]) => { - setDrawnPdfs(pdfFiles) + const onDrawFieldsChange = (sigitFiles: SigitFile[]) => { + setDrawnFiles(sigitFiles) } if (authUrl) { diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 2bc7d49..0e0b135 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -33,14 +33,11 @@ import { sendNotification, signEventForMetaFile, updateUsersAppData, - findOtherUserMarks, - extractFileExtension + findOtherUserMarks } from '../../utils' import { Container } from '../../components/Container' import { DisplayMeta } from './internal/displayMeta' import styles from './style.module.scss' -import { PdfFile } from '../../types/drawing.ts' -import { convertToPdfFile, toFile } from '../../utils/pdf.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts' import { getLastSignersSig } from '../../utils/sign.ts' import { @@ -50,7 +47,11 @@ import { updateMarks } from '../../utils' import PdfMarking from '../../components/PDFView/PdfMarking.tsx' -import { getZipWithFiles } from '../../utils/file.ts' +import { + convertToSigitFile, + getZipWithFiles, + SigitFile +} from '../../utils/file.ts' import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts' enum SignedStatus { Fully_Signed, @@ -77,7 +78,7 @@ export const SignPage = () => { const [selectedFile, setSelectedFile] = useState(null) - const [files, setFiles] = useState<{ [filename: string]: PdfFile | File }>({}) + const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({}) const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -403,7 +404,7 @@ export const SignPage = () => { return } - const files: { [filename: string]: PdfFile | File } = {} + const files: { [filename: string]: SigitFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files).map((entry) => entry.name) @@ -417,15 +418,7 @@ export const SignPage = () => { ) if (arrayBuffer) { - try { - files[fileName] = await convertToPdfFile(arrayBuffer, fileName) - } catch (error) { - files[fileName] = toFile( - arrayBuffer, - fileName, - 'application/' + extractFileExtension(fileName) - ) - } + files[fileName] = await convertToSigitFile(arrayBuffer, fileName) const hash = await getHash(arrayBuffer) if (hash) { fileHashes[fileName] = hash @@ -470,7 +463,7 @@ export const SignPage = () => { const zip = await loadZip(decryptedZipFile) if (!zip) return - const files: { [filename: string]: PdfFile } = {} + const files: { [filename: string]: SigitFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files) .filter((entry) => entry.name.startsWith('files/') && !entry.dir) @@ -487,7 +480,7 @@ export const SignPage = () => { ) if (arrayBuffer) { - files[fileName] = await convertToPdfFile(arrayBuffer, fileName) + files[fileName] = await convertToSigitFile(arrayBuffer, fileName) const hash = await getHash(arrayBuffer) if (hash) { @@ -773,11 +766,7 @@ export const SignPage = () => { zip.file('meta.json', stringifiedMeta) for (const [fileName, file] of Object.entries(files)) { - if ('pages' in file) { - zip.file(`files/${fileName}`, await file.file.arrayBuffer()) - } else { - zip.file(`files/${fileName}`, await file.arrayBuffer()) - } + zip.file(`files/${fileName}`, await file.arrayBuffer()) } const arrayBuffer = await zip @@ -815,11 +804,7 @@ export const SignPage = () => { zip.file('meta.json', stringifiedMeta) for (const [fileName, file] of Object.entries(files)) { - if ('pages' in file) { - zip.file(`files/${fileName}`, await file.file.arrayBuffer()) - } else { - zip.file(`files/${fileName}`, await file.arrayBuffer()) - } + zip.file(`files/${fileName}`, await file.arrayBuffer()) } const arrayBuffer = await zip diff --git a/src/pages/sign/internal/displayMeta.tsx b/src/pages/sign/internal/displayMeta.tsx index 8e09612..bb298c6 100644 --- a/src/pages/sign/internal/displayMeta.tsx +++ b/src/pages/sign/internal/displayMeta.tsx @@ -34,11 +34,11 @@ import { UserAvatar } from '../../../components/UserAvatar' import { MetadataController } from '../../../controllers' import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils' import styles from '../style.module.scss' -import { PdfFile } from '../../../types/drawing.ts' +import { SigitFile } from '../../../utils/file' type DisplayMetaProps = { meta: Meta - files: { [filename: string]: PdfFile | File } + files: { [fileName: string]: SigitFile } submittedBy: string signers: `npub1${string}`[] viewers: `npub1${string}`[] @@ -143,19 +143,9 @@ export const DisplayMeta = ({ }) }, [users, submittedBy, metadata]) - const downloadFile = async (filename: string) => { - const file = files[filename] - - let arrayBuffer: ArrayBuffer - if ('pages' in file) { - arrayBuffer = await file.file.arrayBuffer() - } else { - arrayBuffer = await file.arrayBuffer() - } - if (!arrayBuffer) return - - const blob = new Blob([arrayBuffer]) - saveAs(blob, filename) + const downloadFile = async (fileName: string) => { + const file = files[fileName] + saveAs(file) } return ( diff --git a/src/pages/sign/style.module.scss b/src/pages/sign/style.module.scss index 1dbc6c5..dffb039 100644 --- a/src/pages/sign/style.module.scss +++ b/src/pages/sign/style.module.scss @@ -2,8 +2,6 @@ .container { color: $text-color; - //width: 550px; - //max-width: 550px; .inputBlock { position: relative; @@ -67,7 +65,7 @@ //z-index: 200; } - .fixedBottomForm input[type="text"] { + .fixedBottomForm input[type='text'] { width: 80%; padding: 10px; font-size: 16px; diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 24bbc76..172ce42 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Divider, Tooltip, Typography } from '@mui/material' +import { Box, Button, Tooltip, Typography } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' import { Event, verifyEvent } from 'nostr-tools' @@ -21,20 +21,16 @@ import { readContentOfZipEntry, signEventForMetaFile, shorten, - getCurrentUserFiles, - extractFileExtension + getCurrentUserFiles } from '../../utils' import styles from './style.module.scss' import { useLocation } from 'react-router-dom' import axios from 'axios' -import { PdfFile } from '../../types/drawing.ts' import { addMarks, convertToPdfBlob, - convertToPdfFile, groupMarksByFileNamePage, - inPx, - toFile + inPx } from '../../utils/pdf.ts' import { State } from '../../store/rootReducer.ts' import { useSelector } from 'react-redux' @@ -51,6 +47,9 @@ import FileList from '../../components/FileList' import { CurrentUserFile } from '../../types/file.ts' import { Mark } from '../../types/mark.ts' import React from 'react' +import { convertToSigitFile, SigitFile } from '../../utils/file.ts' +import { FileDivider } from '../../components/FileDivider.tsx' +import { ExtensionFileBox } from '../../components/ExtensionFileBox.tsx' interface PdfViewProps { files: CurrentUserFile[] @@ -69,26 +68,25 @@ const SlimPdfView = ({ useEffect(() => { if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { pdfRefs.current[currentFile.id]?.scrollIntoView({ - behavior: 'smooth', - block: 'end' + behavior: 'smooth' }) } }, [currentFile]) return ( -
+
{files.map((currentUserFile, i) => { - const { hash, filename, pdfFile, id } = currentUserFile + const { hash, file, id } = currentUserFile const signatureEvents = Object.keys(parsedSignatureEvents) if (!hash) return return ( - +
(pdfRefs.current[id] = el)} - className={styles.fileWrapper} + className="file-wrapper" > - {'pages' in pdfFile ? ( - pdfFile.pages.map((page, i) => { + {file.isPdf ? ( + file.pages?.map((page, i) => { const marks: Mark[] = [] signatureEvents.forEach((e) => { @@ -102,7 +100,7 @@ const SlimPdfView = ({ } }) return ( -
+
{marks.map((m) => { return ( @@ -124,22 +122,11 @@ const SlimPdfView = ({ ) }) ) : ( -
- This is a {extractFileExtension(pdfFile.name)} file -
+ )}
- {i < files.length - 1 && ( - - File Separator - - )} + {i < files.length - 1 && } ) })} @@ -179,7 +166,7 @@ export const VerifyPage = () => { const [currentFileHashes, setCurrentFileHashes] = useState<{ [key: string]: string | null }>({}) - const [files, setFiles] = useState<{ [filename: string]: PdfFile | File }>({}) + const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({}) const [currentFile, setCurrentFile] = useState(null) const [signatureFileHashes, setSignatureFileHashes] = useState<{ [key: string]: string @@ -238,7 +225,7 @@ export const VerifyPage = () => { if (!zip) return - const files: { [filename: string]: PdfFile | File } = {} + const files: { [fileName: string]: SigitFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files).map( (entry) => entry.name @@ -254,18 +241,10 @@ export const VerifyPage = () => { ) if (arrayBuffer) { - try { - files[fileName] = await convertToPdfFile( - arrayBuffer, - fileName! - ) - } catch (error) { - files[fileName] = toFile( - arrayBuffer, - fileName, - 'application/' + extractFileExtension(fileName) - ) - } + files[fileName] = await convertToSigitFile( + arrayBuffer, + fileName! + ) const hash = await getHash(arrayBuffer) if (hash) { @@ -439,15 +418,15 @@ export const VerifyPage = () => { const marks = extractMarksFromSignedMeta(updatedMeta) const marksByPage = groupMarksByFileNamePage(marks) - for (const [fileName, pdf] of Object.entries(files)) { - let blob: Blob - if ('pages' in pdf) { - const pages = await addMarks(pdf.file, marksByPage[fileName]) - blob = await convertToPdfBlob(pages) + for (const [fileName, file] of Object.entries(files)) { + if (file.isPdf) { + // Draw marks into PDF file and generate a brand new blob + const pages = await addMarks(file, marksByPage[fileName]) + const blob = await convertToPdfBlob(pages) + zip.file(`files/${fileName}`, blob) } else { - blob = new Blob([pdf], { type: pdf.type }) + zip.file(`files/${fileName}`, file) } - zip.file(`files/${fileName}`, blob) } const arrayBuffer = await zip diff --git a/src/pages/verify/style.module.scss b/src/pages/verify/style.module.scss index a3d3401..af93107 100644 --- a/src/pages/verify/style.module.scss +++ b/src/pages/verify/style.module.scss @@ -51,30 +51,6 @@ } } -.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; diff --git a/src/types/drawing.ts b/src/types/drawing.ts index 1e65038..b8abe73 100644 --- a/src/types/drawing.ts +++ b/src/types/drawing.ts @@ -8,12 +8,6 @@ export interface MouseState { } } -export interface PdfFile { - file: File - pages: PdfPage[] - expanded?: boolean -} - export interface PdfPage { image: string drawnFields: DrawnField[] diff --git a/src/types/file.ts b/src/types/file.ts index bbb19b6..4553b23 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -1,9 +1,8 @@ -import { PdfFile } from './drawing.ts' +import { SigitFile } from '../utils/file' export interface CurrentUserFile { id: number - pdfFile: PdfFile | File - filename: string + file: SigitFile hash?: string isHashValid: boolean } diff --git a/src/utils/const.ts b/src/utils/const.ts index 2940399..83aca9e 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -26,3 +26,94 @@ export const DEFAULT_LOOK_UP_RELAY_LIST = [ 'wss://user.kindpag.es', 'wss://purplepag.es' ] + +// Uses the https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types list +// Updated on 2024/08/22 +export const MOST_COMMON_MEDIA_TYPES = new Map([ + ['aac', 'audio/aac'], // AAC audio + ['abw', 'application/x-abiword'], // AbiWord document + ['apng', 'image/apng'], // Animated Portable Network Graphics (APNG) image + ['arc', 'application/x-freearc'], // Archive document (multiple files embedded) + ['avif', 'image/avif'], // AVIF image + ['avi', 'video/x-msvideo'], // AVI: Audio Video Interleave + ['azw', 'application/vnd.amazon.ebook'], // Amazon Kindle eBook format + ['bin', 'application/octet-stream'], // Any kind of binary data + ['bmp', 'image/bmp'], // Windows OS/2 Bitmap Graphics + ['bz', 'application/x-bzip'], // BZip archive + ['bz2', 'application/x-bzip2'], // BZip2 archive + ['cda', 'application/x-cdf'], // CD audio + ['csh', 'application/x-csh'], // C-Shell script + ['css', 'text/css'], // Cascading Style Sheets (CSS) + ['csv', 'text/csv'], // Comma-separated values (CSV) + ['doc', 'application/msword'], // Microsoft Word + [ + 'docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + ], // Microsoft Word (OpenXML) + ['eot', 'application/vnd.ms-fontobject'], // MS Embedded OpenType fonts + ['epub', 'application/epub+zip'], // Electronic publication (EPUB) + ['gz', 'application/gzip'], // GZip Compressed Archive + ['gif', 'image/gif'], // Graphics Interchange Format (GIF) + ['htm', 'text/html'], // HyperText Markup Language (HTML) + ['html', 'text/html'], // HyperText Markup Language (HTML) + ['ico', 'image/vnd.microsoft.icon'], // Icon format + ['ics', 'text/calendar'], // iCalendar format + ['jar', 'application/java-archive'], // Java Archive (JAR) + ['jpeg', 'image/jpeg'], // JPEG images + ['jpg', 'image/jpeg'], // JPEG images + ['js', 'text/javascript'], // JavaScript + ['json', 'application/json'], // JSON format + ['jsonld', 'application/ld+json'], // JSON-LD format + ['mid', 'audio/midi'], // Musical Instrument Digital Interface (MIDI) + ['midi', 'audio/midi'], // Musical Instrument Digital Interface (MIDI) + ['mjs', 'text/javascript'], // JavaScript module + ['mp3', 'audio/mpeg'], // MP3 audio + ['mp4', 'video/mp4'], // MP4 video + ['mpeg', 'video/mpeg'], // MPEG Video + ['mpkg', 'application/vnd.apple.installer+xml'], // Apple Installer Package + ['odp', 'application/vnd.oasis.opendocument.presentation'], // OpenDocument presentation document + ['ods', 'application/vnd.oasis.opendocument.spreadsheet'], // OpenDocument spreadsheet document + ['odt', 'application/vnd.oasis.opendocument.text'], // OpenDocument text document + ['oga', 'audio/ogg'], // Ogg audio + ['ogv', 'video/ogg'], // Ogg video + ['ogx', 'application/ogg'], // Ogg + ['opus', 'audio/ogg'], // Opus audio in Ogg container + ['otf', 'font/otf'], // OpenType font + ['png', 'image/png'], // Portable Network Graphics + ['pdf', 'application/pdf'], // Adobe Portable Document Format (PDF) + ['php', 'application/x-httpd-php'], // Hypertext Preprocessor (Personal Home Page) + ['ppt', 'application/vnd.ms-powerpoint'], // Microsoft PowerPoint + [ + 'pptx', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + ], // Microsoft PowerPoint (OpenXML) + ['rar', 'application/vnd.rar'], // RAR archive + ['rtf', 'application/rtf'], // Rich Text Format (RTF) + ['sh', 'application/x-sh'], // Bourne shell script + ['svg', 'image/svg+xml'], // Scalable Vector Graphics (SVG) + ['tar', 'application/x-tar'], // Tape Archive (TAR) + ['tif', 'image/tiff'], // Tagged Image File Format (TIFF) + ['tiff', 'image/tiff'], // Tagged Image File Format (TIFF) + ['ts', 'video/mp2t'], // MPEG transport stream + ['ttf', 'font/ttf'], // TrueType Font + ['txt', 'text/plain'], // Text, (generally ASCII or ISO 8859-n) + ['vsd', 'application/vnd.visio'], // Microsoft Visio + ['wav', 'audio/wav'], // Waveform Audio Format + ['weba', 'audio/webm'], // WEBM audio + ['webm', 'video/webm'], // WEBM video + ['webp', 'image/webp'], // WEBP image + ['woff', 'font/woff'], // Web Open Font Format (WOFF) + ['woff2', 'font/woff2'], // Web Open Font Format (WOFF) + ['xhtml', 'application/xhtml+xml'], // XHTML + ['xls', 'application/vnd.ms-excel'], // Microsoft Excel + [ + '.xlsx', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ], // Microsoft Excel (OpenXML) + ['xml', 'application/xml'], // XML + ['xul', 'application/vnd.mozilla.xul+xml'], // XUL + ['zip', 'application/zip'], // ZIP archive + ['3gp', 'video/3gpp'], // 3GPP audio/video container + ['3g2', 'video/3gpp2'], // 3GPP2 audio/video container + ['7z', 'application/x-7z-compressed'] // 7-zip archive +]) diff --git a/src/utils/file.ts b/src/utils/file.ts index c50f2e9..d84524d 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,32 +1,120 @@ import { Meta } from '../types' +import { PdfPage } from '../types/drawing.ts' +import { MOST_COMMON_MEDIA_TYPES } from './const.ts' import { extractMarksFromSignedMeta } from './mark.ts' -import { addMarks, convertToPdfBlob, groupMarksByFileNamePage } from './pdf.ts' +import { + addMarks, + convertToPdfBlob, + groupMarksByFileNamePage, + isPdf, + pdfToImages +} from './pdf.ts' import JSZip from 'jszip' -import { PdfFile } from '../types/drawing.ts' -const getZipWithFiles = async ( +export const getZipWithFiles = async ( meta: Meta, - files: { [filename: string]: PdfFile | File } + files: { [filename: string]: SigitFile } ): Promise => { const zip = new JSZip() const marks = extractMarksFromSignedMeta(meta) const marksByFileNamePage = groupMarksByFileNamePage(marks) for (const [fileName, file] of Object.entries(files)) { - let blob: Blob - if ('pages' in file) { + if (file.isPdf) { // Handle PDF Files - const pages = await addMarks(file.file, marksByFileNamePage[fileName]) - blob = await convertToPdfBlob(pages) + const pages = await addMarks(file, marksByFileNamePage[fileName]) + const blob = await convertToPdfBlob(pages) + zip.file(`files/${fileName}`, blob) } else { // Handle other files - blob = new Blob([file], { type: file.type }) + zip.file(`files/${fileName}`, file) } - - zip.file(`files/${fileName}`, blob) } return zip } -export { getZipWithFiles } +/** + * Converts a PDF ArrayBuffer to a generic PDF File + * @param arrayBuffer of a PDF + * @param fileName identifier of the pdf file + * @param type optional file type (defaults to pdf) + */ +export const toFile = ( + arrayBuffer: ArrayBuffer, + fileName: string, + type: string = 'application/pdf' +): File => { + const blob = new Blob([arrayBuffer], { type }) + return new File([blob], fileName, { type }) +} + +export class SigitFile extends File { + extension: string + pages?: PdfPage[] + isPdf: boolean + + constructor(file: File) { + super([file], file.name, { type: file.type }) + this.isPdf = isPdf(this) + this.extension = extractFileExtension(this.name) + } + + async process() { + if (this.isPdf) this.pages = await pdfToImages(await this.arrayBuffer()) + } +} + +export const getSigitFile = async (file: File) => { + const sigitFile = new SigitFile(file) + // Process sigit file + // - generate pages for PDF files + await sigitFile.process() + return sigitFile +} + +/** + * Takes an ArrayBuffer and converts to Sigit's Internal File type + * @param arrayBuffer + * @param fileName + */ +export const convertToSigitFile = async ( + arrayBuffer: ArrayBuffer, + fileName: string +): Promise => { + const type = getMediaType(extractFileExtension(fileName)) + const file = toFile(arrayBuffer, fileName, type) + const sigitFile = await getSigitFile(file) + return sigitFile +} + +/** + * @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() + if (extension) { + result.push(extension) + } + return result + }, []) + + const isSame = extensions.every((ext) => ext === extensions[0]) + + return { extensions, isSame } +} + +/** + * @param fileName - Filename to check + * @returns Extension string + */ +export const extractFileExtension = (fileName: string) => { + const parts = fileName.split('.') + return parts[parts.length - 1] +} + +export const getMediaType = (extension: string) => { + return MOST_COMMON_MEDIA_TYPES.get(extension) +} diff --git a/src/utils/meta.ts b/src/utils/meta.ts index 0bee969..fd3481c 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -2,6 +2,7 @@ import { CreateSignatureEventContent, Meta } from '../types' import { fromUnixTimestamp, parseJson } from '.' import { Event, verifyEvent } from 'nostr-tools' import { toast } from 'react-toastify' +import { extractFileExtensions } from './file' export enum SignStatus { Signed = 'Signed', @@ -172,29 +173,3 @@ 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() - if (extension) { - result.push(extension) - } - return result - }, []) - - const isSame = extensions.every((ext) => ext === extensions[0]) - - return { extensions, isSame } -} - -/** - * @param fileName - Filename to check - * @returns Extension string - */ -export const extractFileExtension = (fileName: string) => { - return fileName.split('.').pop() -} diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index 05daa3e..8abafae 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -1,4 +1,4 @@ -import { PdfFile, PdfPage } from '../types/drawing.ts' +import { PdfPage } from '../types/drawing.ts' import * as PDFJS from 'pdfjs-dist' import { PDFDocument } from 'pdf-lib' import { Mark } from '../types/mark.ts' @@ -12,7 +12,7 @@ PDFJS.GlobalWorkerOptions.workerSrc = new URL( * Scale between the PDF page's natural size and rendered size * @constant {number} */ -const SCALE: number = 3 +export const SCALE: number = 3 /** * Defined font size used when generating a PDF. Currently it is difficult to fully * correlate font size used at the time of filling in / drawing on the PDF @@ -20,63 +20,28 @@ const SCALE: number = 3 * This should be fixed going forward. * Switching to PDF-Lib will most likely make this problem redundant. */ -const FONT_SIZE: number = 40 +export const FONT_SIZE: number = 40 /** * Current font type used when generating a PDF. */ -const FONT_TYPE: string = 'Arial' - -/** - * Converts a PDF ArrayBuffer to a generic PDF File - * @param arrayBuffer of a PDF - * @param fileName identifier of the pdf file - * @param type optional file type (defaults to pdf) - */ -const toFile = ( - arrayBuffer: ArrayBuffer, - fileName: string, - type: string = 'application/pdf' -): File => { - const blob = new Blob([arrayBuffer], { type }) - return new File([blob], fileName, { type }) -} - -/** - * Converts a generic PDF File to Sigit's internal Pdf File type - * @param {File} file - * @return {PdfFile} Sigit's internal PDF File type - */ -const toPdfFile = async (file: File): Promise => { - const data = await readPdf(file) - const pages = await pdfToImages(data) - return { file, pages, expanded: false } -} -/** - * Transforms an array of generic PDF Files into an array of Sigit's - * internal representation of Pdf Files - * @param selectedFiles - an array of generic PDF Files - * @return PdfFile[] - an array of Sigit's internal Pdf File type - */ -const toPdfFiles = async (selectedFiles: File[]): Promise => { - return Promise.all(selectedFiles.filter(isPdf).map(toPdfFile)) -} +export const FONT_TYPE: string = 'Arial' /** * A utility that transforms a drawing coordinate number into a CSS-compatible string * @param coordinate */ -const inPx = (coordinate: number): string => `${coordinate}px` +export const inPx = (coordinate: number): string => `${coordinate}px` /** * A utility that checks if a given file is of the pdf type * @param file */ -const isPdf = (file: File) => file.type.toLowerCase().includes('pdf') +export const isPdf = (file: File) => file.type.toLowerCase().includes('pdf') /** * Reads the pdf file binaries */ -const readPdf = (file: File): Promise => { +export const readPdf = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader() @@ -104,7 +69,9 @@ const readPdf = (file: File): Promise => { * Converts pdf to the images * @param data pdf file bytes */ -const pdfToImages = async (data: string | ArrayBuffer): Promise => { +export const pdfToImages = async ( + data: string | ArrayBuffer +): Promise => { const images: string[] = [] const pdf = await PDFJS.getDocument(data).promise const canvas = document.createElement('canvas') @@ -134,7 +101,7 @@ const pdfToImages = async (data: string | ArrayBuffer): Promise => { * Returns an array of encoded images where each image is a representation * of a PDF page with completed and signed marks from all users */ -const addMarks = async ( +export const addMarks = async ( file: File, marksPerPage: { [key: string]: Mark[] } ) => { @@ -164,7 +131,7 @@ const addMarks = async ( /** * Utility to scale mark in line with the PDF-to-PNG scale */ -const scaleMark = (mark: Mark): Mark => { +export const scaleMark = (mark: Mark): Mark => { const { location } = mark return { ...mark, @@ -182,14 +149,14 @@ const scaleMark = (mark: Mark): Mark => { * Utility to check if a Mark has value * @param mark */ -const hasValue = (mark: Mark): boolean => !!mark.value +export const hasValue = (mark: Mark): boolean => !!mark.value /** * Draws a Mark on a Canvas representation of a PDF Page * @param mark to be drawn * @param ctx a Canvas representation of a specific PDF Page */ -const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { +export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { const { location } = mark ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE @@ -204,7 +171,9 @@ const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { * Takes an array of encoded PDF pages and returns a blob that is a complete PDF file * @param markedPdfPages */ -const convertToPdfBlob = async (markedPdfPages: string[]): Promise => { +export const convertToPdfBlob = async ( + markedPdfPages: string[] +): Promise => { const pdfDoc = await PDFDocument.create() for (const page of markedPdfPages) { @@ -222,30 +191,17 @@ const convertToPdfBlob = async (markedPdfPages: string[]): Promise => { return new Blob([pdfBytes], { type: 'application/pdf' }) } -/** - * Takes an ArrayBuffer of a PDF file and converts to Sigit's Internal Pdf File type - * @param arrayBuffer - * @param fileName - */ -const convertToPdfFile = async ( - arrayBuffer: ArrayBuffer, - fileName: string -): Promise => { - const file = toFile(arrayBuffer, fileName) - return toPdfFile(file) -} - /** * @param marks - an array of Marks * @function hasValue removes any Mark without a property * @function scaleMark scales remaining marks in line with SCALE * @function byPage groups remaining Marks by their page marks.location.page */ -const groupMarksByFileNamePage = (marks: Mark[]) => { +export const groupMarksByFileNamePage = (marks: Mark[]) => { return marks .filter(hasValue) .map(scaleMark) - .reduce<{ [filename: string]: { [page: number]: Mark[] } }>(byPage, {}) + .reduce<{ [fileName: string]: { [page: number]: Mark[] } }>(byPage, {}) } /** @@ -256,30 +212,19 @@ const groupMarksByFileNamePage = (marks: Mark[]) => { * @param obj - accumulator in the reducer callback * @param mark - current value, i.e. Mark being examined */ -const byPage = ( +export const byPage = ( obj: { [filename: string]: { [page: number]: Mark[] } }, mark: Mark ) => { - const filename = mark.fileName + const fileName = mark.fileName const pageNumber = mark.location.page - const pages = obj[filename] ?? {} + const pages = obj[fileName] ?? {} const marks = pages[pageNumber] ?? [] return { ...obj, - [filename]: { + [fileName]: { ...pages, [pageNumber]: [...marks, mark] } } } - -export { - toFile, - toPdfFile, - toPdfFiles, - inPx, - convertToPdfFile, - addMarks, - convertToPdfBlob, - groupMarksByFileNamePage -} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index c16e232..f32e14e 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,5 +1,5 @@ -import { PdfFile } from '../types/drawing.ts' import { CurrentUserFile } from '../types/file.ts' +import { SigitFile } from './file.ts' export const compareObjects = ( obj1: object | null | undefined, @@ -75,13 +75,13 @@ export const timeout = (ms: number = 60000) => { * @param creatorFileHashes */ export const getCurrentUserFiles = ( - files: { [filename: string]: PdfFile | File }, + files: { [filename: string]: SigitFile }, fileHashes: { [key: string]: string | null }, creatorFileHashes: { [key: string]: string } ): CurrentUserFile[] => { - return Object.entries(files).map(([filename, pdfFile], index) => { + return Object.entries(files).map(([filename, file], index) => { return { - pdfFile, + file, filename, id: index + 1, ...(!!fileHashes[filename] && { hash: fileHashes[filename]! }), @@ -89,3 +89,32 @@ export const getCurrentUserFiles = ( } }) } + +/** + * Utility function that generates a promise with a callback on each array item + * and retuns only non-null fulfilled results + * @param array + * @param cb callback that generates a promise + * @returns Array with the non-null results + */ +export const settleAllFullfilfedPromises = async ( + array: Item[], + cb: (arg: Item) => Promise +) => { + // Run the callback on the array to get promises + const promises = array.map(cb) + + // Use Promise.allSettled to wait for all promises to settle + const results = await Promise.allSettled(promises) + + // Extract non-null values from fulfilled promises in a single pass + return results.reduce[]>((acc, result) => { + if (result.status === 'fulfilled') { + const value = result.value + if (value) { + acc.push(value) + } + } + return acc + }, []) +}