fix: show other file types in the middle section (create, sign, verify) #163

Merged
enes merged 5 commits from 138-other-file-types into staging 2024-08-26 12:00:33 +00:00
27 changed files with 632 additions and 518 deletions

View File

@ -69,3 +69,73 @@ a {
input { input {
font-family: inherit; 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;
}
}
// For pdf marks
.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 */
}
}
// For image rendering (uploaded image as a file)
.file-image {
-webkit-user-select: none;
user-select: none;
display: block;
width: 100%;
height: auto;
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;
}

View File

@ -10,7 +10,8 @@ import {
faCalendar, faCalendar,
faCopy, faCopy,
faEye, faEye,
faFile faFile,
faFileCircleExclamation
} 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 { UserAvatarGroup } from '../UserAvatarGroup' import { UserAvatarGroup } from '../UserAvatarGroup'
@ -20,6 +21,7 @@ import { TooltipChild } from '../TooltipChild'
import { getExtensionIconLabel } from '../getExtensionIconLabel' import { getExtensionIconLabel } from '../getExtensionIconLabel'
import { useSigitProfiles } from '../../hooks/useSigitProfiles' import { useSigitProfiles } from '../../hooks/useSigitProfiles'
import { useSigitMeta } from '../../hooks/useSigitMeta' import { useSigitMeta } from '../../hooks/useSigitMeta'
import { extractFileExtensions } from '../../utils/file'
type SigitProps = { type SigitProps = {
meta: Meta meta: Meta
@ -27,23 +29,18 @@ type SigitProps = {
} }
export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
const { const { title, createdAt, submittedBy, signers, signedStatus, isValid } =
title, parsedMeta
createdAt,
submittedBy,
signers,
signedStatus,
fileExtensions,
isValid
} = parsedMeta
const { signersStatus } = useSigitMeta(meta) const { signersStatus, fileHashes } = useSigitMeta(meta)
const profiles = useSigitProfiles([ const profiles = useSigitProfiles([
...(submittedBy ? [submittedBy] : []), ...(submittedBy ? [submittedBy] : []),
...signers ...signers
]) ])
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
return ( return (
<div className={styles.itemWrapper}> <div className={styles.itemWrapper}>
<Link <Link
@ -120,17 +117,21 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
<span className={styles.iconLabel}> <span className={styles.iconLabel}>
<FontAwesomeIcon icon={faEye} /> {signedStatus} <FontAwesomeIcon icon={faEye} /> {signedStatus}
</span> </span>
{fileExtensions.length > 0 ? ( {extensions.length > 0 ? (
<span className={styles.iconLabel}> <span className={styles.iconLabel}>
{fileExtensions.length > 1 ? ( {!isSame ? (
<> <>
<FontAwesomeIcon icon={faFile} /> Multiple File Types <FontAwesomeIcon icon={faFile} /> Multiple File Types
</> </>
) : ( ) : (
getExtensionIconLabel(fileExtensions[0]) getExtensionIconLabel(extensions[0])
)} )}
</span> </span>
) : null} ) : (
<>
<FontAwesomeIcon icon={faFileCircleExclamation} /> &mdash;
</>
)}
</div> </div>
<div className={styles.itemActions}> <div className={styles.itemActions}>
<Tooltip title="Duplicate" arrow placement="top" disableInteractive> <Tooltip title="Duplicate" arrow placement="top" disableInteractive>

View File

@ -2,7 +2,6 @@ import { Close } from '@mui/icons-material'
import { import {
Box, Box,
CircularProgress, CircularProgress,
Divider,
FormControl, FormControl,
InputLabel, InputLabel,
MenuItem, MenuItem,
@ -10,19 +9,15 @@ import {
} from '@mui/material' } from '@mui/material'
import styles from './style.module.scss' import styles from './style.module.scss'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import * as PDFJS from 'pdfjs-dist' import * as PDFJS from 'pdfjs-dist'
import { ProfileMetadata, User, UserRole } from '../../types' import { ProfileMetadata, User, UserRole } from '../../types'
import { import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
PdfFile,
MouseState,
PdfPage,
DrawnField,
DrawTool
} from '../../types/drawing'
import { truncate } from 'lodash' import { truncate } from 'lodash'
import { extractFileExtension, hexToNpub } from '../../utils' import { settleAllFullfilfedPromises, hexToNpub } from '../../utils'
import { toPdfFiles } from '../../utils/pdf.ts' import { getSigitFile, SigitFile } from '../../utils/file'
import { FileDivider } from '../FileDivider'
import { ExtensionFileBox } from '../ExtensionFileBox'
PDFJS.GlobalWorkerOptions.workerSrc = new URL( PDFJS.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url import.meta.url
@ -32,15 +27,15 @@ interface Props {
selectedFiles: File[] selectedFiles: File[]
users: User[] users: User[]
metadata: { [key: string]: ProfileMetadata } metadata: { [key: string]: ProfileMetadata }
onDrawFieldsChange: (pdfFiles: PdfFile[]) => void onDrawFieldsChange: (sigitFiles: SigitFile[]) => void
selectedTool?: DrawTool selectedTool?: DrawTool
} }
export const DrawPDFFields = (props: Props) => { export const DrawPDFFields = (props: Props) => {
const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props
const [pdfFiles, setPdfFiles] = useState<PdfFile[]>([]) const [sigitFiles, setSigitFiles] = useState<SigitFile[]>([])
const [parsingPdf, setParsingPdf] = useState<boolean>(false) const [parsingPdf, setIsParsing] = useState<boolean>(false)
const [mouseState, setMouseState] = useState<MouseState>({ const [mouseState, setMouseState] = useState<MouseState>({
clicked: false clicked: false
@ -49,42 +44,43 @@ export const DrawPDFFields = (props: Props) => {
useEffect(() => { useEffect(() => {
if (selectedFiles) { if (selectedFiles) {
/** /**
* Reads the pdf binary files and converts it's pages to images * Reads the binary files and converts to internal file type
* creates the pdfFiles object and sets to a state * and sets to a state (adds images if it's a PDF)
*/ */
const parsePdfPages = async () => { const parsePages = async () => {
const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles) const files = await settleAllFullfilfedPromises(
selectedFiles,
getSigitFile
)
setPdfFiles(pdfFiles) setSigitFiles(files)
} }
setParsingPdf(true) setIsParsing(true)
parsePdfPages().finally(() => { parsePages().finally(() => {
setParsingPdf(false) setIsParsing(false)
}) })
} }
}, [selectedFiles]) }, [selectedFiles])
useEffect(() => { useEffect(() => {
if (pdfFiles) onDrawFieldsChange(pdfFiles) if (sigitFiles) onDrawFieldsChange(sigitFiles)
}, [onDrawFieldsChange, pdfFiles]) }, [onDrawFieldsChange, sigitFiles])
/** /**
* Drawing events * Drawing events
*/ */
useEffect(() => { useEffect(() => {
// window.addEventListener('mousedown', onMouseDown);
window.addEventListener('mouseup', onMouseUp) window.addEventListener('mouseup', onMouseUp)
return () => { return () => {
// window.removeEventListener('mousedown', onMouseDown);
window.removeEventListener('mouseup', onMouseUp) window.removeEventListener('mouseup', onMouseUp)
} }
}, []) }, [])
const refreshPdfFiles = () => { const refreshPdfFiles = () => {
setPdfFiles([...pdfFiles]) setSigitFiles([...sigitFiles])
} }
/** /**
@ -303,10 +299,10 @@ export const DrawPDFFields = (props: Props) => {
) => { ) => {
event.stopPropagation() event.stopPropagation()
pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice( const pages = sigitFiles[pdfFileIndex]?.pages
drawnFileIndex, if (pages) {
1 pages[pdfPageIndex]?.drawnFields?.splice(drawnFileIndex, 1)
) }
} }
/** /**
@ -345,14 +341,17 @@ export const DrawPDFFields = (props: Props) => {
/** /**
* Renders the pdf pages and drawing elements * 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 ( return (
<> <>
{pdfFile.pages.map((page, pdfPageIndex: number) => { {file.pages?.map((page, pageIndex: number) => {
return ( return (
<div <div
key={pdfPageIndex} key={pageIndex}
className={`${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`} className={`image-wrapper ${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`}
> >
<img <img
onMouseMove={(event) => { onMouseMove={(event) => {
@ -393,8 +392,8 @@ export const DrawPDFFields = (props: Props) => {
onMouseDown={(event) => { onMouseDown={(event) => {
onRemoveHandleMouseDown( onRemoveHandleMouseDown(
event, event,
pdfFileIndex, fileIndex,
pdfPageIndex, pageIndex,
drawnFieldIndex drawnFieldIndex
) )
}} }}
@ -469,40 +468,29 @@ export const DrawPDFFields = (props: Props) => {
) )
} }
if (!pdfFiles.length) { if (!sigitFiles.length) {
return '' return ''
} }
return ( return (
<div className={styles.view}> <div className="files-wrapper">
{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 ( return (
<React.Fragment key={name}> <React.Fragment key={file.name}>
<div <div className="file-wrapper" id={`file-${file.name}`}>
className={`${styles.fileWrapper} ${styles.scrollTarget}`} {file.isPdf && getPdfPages(file, i)}
id={`file-${name}`} {file.isImage && (
> <img
{pdfFile ? ( className="file-image"
getPdfPages(pdfFile, i) src={file.objectUrl}
) : ( alt={file.name}
<div className={styles.otherFile}> />
This is a {extension} file )}
</div> {!(file.isPdf || file.isImage) && (
<ExtensionFileBox extension={file.extension} />
)} )}
</div> </div>
{i < selectedFiles.length - 1 && ( {i < selectedFiles.length - 1 && <FileDivider />}
<Divider
sx={{
fontSize: '12px',
color: 'rgba(0,0,0,0.15)'
}}
>
File Separator
</Divider>
)}
</React.Fragment> </React.Fragment>
) )
})} })}

View File

@ -8,17 +8,6 @@
} }
.pdfImageWrapper { .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 { &.drawing {
cursor: crosshair; cursor: crosshair;
} }
@ -90,29 +79,3 @@
padding: 5px 0; 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;
}
.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;
}

View File

@ -0,0 +1,6 @@
interface ExtensionFileBoxProps {
extension: string
}
export const ExtensionFileBox = ({ extension }: ExtensionFileBoxProps) => (
<div className="extension-file-box">This is a {extension} file</div>
)

View File

@ -0,0 +1,12 @@
import Divider from '@mui/material/Divider/Divider'
export const FileDivider = () => (
<Divider
sx={{
fontSize: '12px',
color: 'rgba(0,0,0,0.15)'
}}
>
File Separator
</Divider>
)

View File

@ -24,19 +24,23 @@ const FileList = ({
<div className={styles.wrap}> <div className={styles.wrap}>
<div className={styles.container}> <div className={styles.container}>
<ul className={styles.files}> <ul className={styles.files}>
{files.map((file: CurrentUserFile) => ( {files.map((currentUserFile: CurrentUserFile) => (
<li <li
key={file.id} key={currentUserFile.id}
className={`${styles.fileItem} ${isActive(file) && styles.active}`} className={`${styles.fileItem} ${isActive(currentUserFile) && styles.active}`}
onClick={() => setCurrentFile(file)} onClick={() => setCurrentFile(currentUserFile)}
> >
<div className={styles.fileNumber}>{file.id}</div> <div className={styles.fileNumber}>{currentUserFile.id}</div>
<div className={styles.fileInfo}> <div className={styles.fileInfo}>
<div className={styles.fileName}>{file.filename}</div> <div className={styles.fileName}>
{currentUserFile.file.name}
</div>
</div> </div>
<div className={styles.fileVisual}> <div className={styles.fileVisual}>
{file.isHashValid && <FontAwesomeIcon icon={faCheck} />} {currentUserFile.isHashValid && (
<FontAwesomeIcon icon={faCheck} />
)}
</div> </div>
</li> </li>
))} ))}

View File

@ -1,12 +1,13 @@
import { PdfFile } from '../../types/drawing.ts'
import { CurrentUserMark, Mark } from '../../types/mark.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts'
import { SigitFile } from '../../utils/file.ts'
import { ExtensionFileBox } from '../ExtensionFileBox.tsx'
import PdfPageItem from './PdfPageItem.tsx' import PdfPageItem from './PdfPageItem.tsx'
interface PdfItemProps { interface PdfItemProps {
currentUserMarks: CurrentUserMark[] currentUserMarks: CurrentUserMark[]
handleMarkClick: (id: number) => void handleMarkClick: (id: number) => void
otherUserMarks: Mark[] otherUserMarks: Mark[]
pdfFile: PdfFile file: SigitFile
selectedMark: CurrentUserMark | null selectedMark: CurrentUserMark | null
selectedMarkValue: string selectedMarkValue: string
} }
@ -15,7 +16,7 @@ interface PdfItemProps {
* Responsible for displaying pages of a single Pdf File. * Responsible for displaying pages of a single Pdf File.
*/ */
const PdfItem = ({ const PdfItem = ({
pdfFile, file,
currentUserMarks, currentUserMarks,
handleMarkClick, handleMarkClick,
selectedMarkValue, selectedMarkValue,
@ -31,19 +32,25 @@ const PdfItem = ({
const filterMarksByPage = (marks: Mark[], page: number): Mark[] => { const filterMarksByPage = (marks: Mark[], page: number): Mark[] => {
return marks.filter((mark) => mark.location.page === page) return marks.filter((mark) => mark.location.page === page)
} }
return pdfFile.pages.map((page, i) => { if (file.isPdf) {
return ( return file.pages?.map((page, i) => {
<PdfPageItem return (
page={page} <PdfPageItem
key={i} page={page}
currentUserMarks={filterByPage(currentUserMarks, i)} key={i}
handleMarkClick={handleMarkClick} currentUserMarks={filterByPage(currentUserMarks, i)}
selectedMarkValue={selectedMarkValue} handleMarkClick={handleMarkClick}
selectedMark={selectedMark} selectedMarkValue={selectedMarkValue}
otherUserMarks={filterMarksByPage(otherUserMarks, i)} selectedMark={selectedMark}
/> otherUserMarks={filterMarksByPage(otherUserMarks, i)}
) />
}) )
})
} else if (file.isImage) {
return <img className="file-image" src={file.objectUrl} alt={file.name} />
} else {
return <ExtensionFileBox extension={file.extension} />
}
} }
export default PdfItem export default PdfItem

View File

@ -10,7 +10,6 @@ import {
import { EMPTY } from '../../utils/const.ts' import { EMPTY } from '../../utils/const.ts'
import { Container } from '../Container' import { Container } from '../Container'
import signPageStyles from '../../pages/sign/style.module.scss' import signPageStyles from '../../pages/sign/style.module.scss'
import styles from './style.module.scss'
import { CurrentUserFile } from '../../types/file.ts' import { CurrentUserFile } from '../../types/file.ts'
import FileList from '../FileList' import FileList from '../FileList'
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
@ -134,21 +133,17 @@ const PdfMarking = (props: PdfMarkingProps) => {
} }
right={meta !== null && <UsersDetails meta={meta} />} right={meta !== null && <UsersDetails meta={meta} />}
> >
<div className={styles.container}> {currentUserMarks?.length > 0 && (
{currentUserMarks?.length > 0 && ( <PdfView
<div className={styles.pdfView}> currentFile={currentFile}
<PdfView files={files}
currentFile={currentFile} handleMarkClick={handleMarkClick}
files={files} selectedMarkValue={selectedMarkValue}
handleMarkClick={handleMarkClick} selectedMark={selectedMark}
selectedMarkValue={selectedMarkValue} currentUserMarks={currentUserMarks}
selectedMark={selectedMark} otherUserMarks={otherUserMarks}
currentUserMarks={currentUserMarks} />
otherUserMarks={otherUserMarks} )}
/>
</div>
)}
</div>
</StickySideColumns> </StickySideColumns>
{selectedMark !== null && ( {selectedMark !== null && (
<MarkFormField <MarkFormField

View File

@ -28,20 +28,14 @@ const PdfPageItem = ({
useEffect(() => { useEffect(() => {
if (selectedMark !== null && !!markRefs.current[selectedMark.id]) { if (selectedMark !== null && !!markRefs.current[selectedMark.id]) {
markRefs.current[selectedMark.id]?.scrollIntoView({ markRefs.current[selectedMark.id]?.scrollIntoView({
behavior: 'smooth', behavior: 'smooth'
block: 'end'
}) })
} }
}, [selectedMark]) }, [selectedMark])
const markRefs = useRef<(HTMLDivElement | null)[]>([]) const markRefs = useRef<(HTMLDivElement | null)[]>([])
return ( return (
<div <div className={`image-wrapper ${styles.pdfImageWrapper}`}>
className={styles.pdfImageWrapper} <img draggable="false" src={page.image} />
style={{
border: '1px solid #c4c4c4'
}}
>
<img draggable="false" src={page.image} style={{ width: '100%' }} />
{currentUserMarks.map((m, i) => ( {currentUserMarks.map((m, i) => (
<div key={i} ref={(el) => (markRefs.current[m.id] = el)}> <div key={i} ref={(el) => (markRefs.current[m.id] = el)}>
<PdfMarkItem <PdfMarkItem

View File

@ -1,8 +1,9 @@
import { Divider } from '@mui/material'
import PdfItem from './PdfItem.tsx' import PdfItem from './PdfItem.tsx'
import { CurrentUserMark, Mark } from '../../types/mark.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts'
import { CurrentUserFile } from '../../types/file.ts' import { CurrentUserFile } from '../../types/file.ts'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { FileDivider } from '../FileDivider.tsx'
import React from 'react'
interface PdfViewProps { interface PdfViewProps {
currentFile: CurrentUserFile | null currentFile: CurrentUserFile | null
@ -29,10 +30,7 @@ const PdfView = ({
const pdfRefs = useRef<(HTMLDivElement | null)[]>([]) const pdfRefs = useRef<(HTMLDivElement | null)[]>([])
useEffect(() => { useEffect(() => {
if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { if (currentFile !== null && !!pdfRefs.current[currentFile.id]) {
pdfRefs.current[currentFile.id]?.scrollIntoView({ pdfRefs.current[currentFile.id]?.scrollIntoView({ behavior: 'smooth' })
behavior: 'smooth',
block: 'end'
})
} }
}, [currentFile]) }, [currentFile])
const filterByFile = ( const filterByFile = (
@ -49,29 +47,32 @@ const PdfView = ({
const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean => const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean =>
index !== files.length - 1 index !== files.length - 1
return ( return (
<> <div className="files-wrapper">
{files.map((currentUserFile, index, arr) => { {files.map((currentUserFile, index, arr) => {
const { hash, pdfFile, id } = currentUserFile const { hash, file, id } = currentUserFile
if (!hash) return if (!hash) return
return ( return (
<div <React.Fragment key={index}>
id={pdfFile.file.name} <div
ref={(el) => (pdfRefs.current[id] = el)} id={file.name}
key={index} className="file-wrapper"
> ref={(el) => (pdfRefs.current[id] = el)}
<PdfItem >
pdfFile={pdfFile} <PdfItem
currentUserMarks={filterByFile(currentUserMarks, hash)} file={file}
selectedMark={selectedMark} currentUserMarks={filterByFile(currentUserMarks, hash)}
handleMarkClick={handleMarkClick} selectedMark={selectedMark}
selectedMarkValue={selectedMarkValue} handleMarkClick={handleMarkClick}
otherUserMarks={filterMarksByFile(otherUserMarks, hash)} selectedMarkValue={selectedMarkValue}
/> otherUserMarks={filterMarksByFile(otherUserMarks, hash)}
{isNotLastPdfFile(index, arr) && <Divider>File Separator</Divider>} />
</div> </div>
{isNotLastPdfFile(index, arr) && <FileDivider />}
</React.Fragment>
) )
})} })}
</> </div>
) )
} }

View File

@ -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 { .container {
display: flex; display: flex;
width: 100%; width: 100%;
flex-direction: column; flex-direction: column;
}
.pdfView {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
gap: 10px;
} }
.otherUserMarksDisplay { .otherUserMarksDisplay {

View File

@ -1,7 +1,6 @@
import { Divider, Tooltip } from '@mui/material' import { Divider, Tooltip } from '@mui/material'
import { useSigitProfiles } from '../../hooks/useSigitProfiles' import { useSigitProfiles } from '../../hooks/useSigitProfiles'
import { import {
extractFileExtensions,
formatTimestamp, formatTimestamp,
fromUnixTimestamp, fromUnixTimestamp,
hexToNpub, hexToNpub,
@ -28,6 +27,7 @@ import { State } from '../../store/rootReducer'
import { TooltipChild } from '../TooltipChild' import { TooltipChild } from '../TooltipChild'
import { DisplaySigner } from '../DisplaySigner' import { DisplaySigner } from '../DisplaySigner'
import { Meta } from '../../types' import { Meta } from '../../types'
import { extractFileExtensions } from '../../utils/file'
interface UsersDetailsProps { interface UsersDetailsProps {
meta: Meta meta: Meta

View File

@ -1,5 +1,9 @@
import { Event, Filter, Relay } from 'nostr-tools' import { Event, Filter, Relay } from 'nostr-tools'
import { normalizeWebSocketURL, timeout } from '../utils' import {
settleAllFullfilfedPromises,
normalizeWebSocketURL,
timeout
} from '../utils'
import { SIGIT_RELAY } from '../utils/const' import { SIGIT_RELAY } from '../utils/const'
/** /**
@ -105,24 +109,11 @@ export class RelayController {
} }
// connect to all specified relays // connect to all specified relays
const relayPromises = relayUrls.map((relayUrl) => const relays = await settleAllFullfilfedPromises(
this.connectRelay(relayUrl) 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<Relay[]>((acc, result) => {
if (result.status === 'fulfilled') {
const value = result.value
if (value) {
acc.push(value)
}
}
return acc
}, [])
// Check if any relays are connected // Check if any relays are connected
if (relays.length === 0) { if (relays.length === 0) {
throw new Error('No relay is connected to fetch events!') throw new Error('No relay is connected to fetch events!')
@ -228,23 +219,10 @@ export class RelayController {
} }
// connect to all specified relays // connect to all specified relays
const relayPromises = relayUrls.map((relayUrl) => { const relays = await settleAllFullfilfedPromises(
return this.connectRelay(relayUrl) 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<Relay[]>((acc, result) => {
if (result.status === 'fulfilled') {
const value = result.value
if (value) {
acc.push(value)
}
}
return acc
}, [])
// Check if any relays are connected // Check if any relays are connected
if (relays.length === 0) { if (relays.length === 0) {
@ -292,24 +270,11 @@ export class RelayController {
} }
// connect to all specified relays // connect to all specified relays
const relayPromises = relayUrls.map((relayUrl) => const relays = await settleAllFullfilfedPromises(
this.connectRelay(relayUrl) 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<Relay[]>((acc, result) => {
if (result.status === 'fulfilled') {
const value = result.value
if (value) {
acc.push(value)
}
}
return acc
}, [])
// Check if any relays are connected // Check if any relays are connected
if (relays.length === 0) { if (relays.length === 0) {
throw new Error('No relay is connected to publish event!') throw new Error('No relay is connected to publish event!')

View File

@ -51,7 +51,7 @@ import {
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import styles from './style.module.scss' import styles from './style.module.scss'
import fileListStyles from '../../components/FileList/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 { DrawPDFFields } from '../../components/DrawPDFFields'
import { Mark } from '../../types/mark.ts' import { Mark } from '../../types/mark.ts'
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
@ -83,6 +83,7 @@ import {
faTrash, faTrash,
faUpload faUpload
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { SigitFile } from '../../utils/file.ts'
export const CreatePage = () => { export const CreatePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
@ -125,7 +126,7 @@ export const CreatePage = () => {
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
{} {}
) )
const [drawnPdfs, setDrawnPdfs] = useState<PdfFile[]>([]) const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
const [selectedTool, setSelectedTool] = useState<DrawTool>() const [selectedTool, setSelectedTool] = useState<DrawTool>()
const [toolbox] = useState<DrawTool[]>([ const [toolbox] = useState<DrawTool[]>([
@ -507,26 +508,28 @@ export const CreatePage = () => {
} }
const createMarks = (fileHashes: { [key: string]: string }): Mark[] => { const createMarks = (fileHashes: { [key: string]: string }): Mark[] => {
return drawnPdfs return drawnFiles
.flatMap((drawnPdf) => { .flatMap((file) => {
const fileHash = fileHashes[drawnPdf.file.name] const fileHash = fileHashes[file.name]
return drawnPdf.pages.flatMap((page, index) => { return (
return page.drawnFields.map((drawnField) => { file.pages?.flatMap((page, index) => {
return { return page.drawnFields.map((drawnField) => {
type: drawnField.type, return {
location: { type: drawnField.type,
page: index, location: {
top: drawnField.top, page: index,
left: drawnField.left, top: drawnField.top,
height: drawnField.height, left: drawnField.left,
width: drawnField.width height: drawnField.height,
}, width: drawnField.width
npub: drawnField.counterpart, },
pdfFileHash: fileHash, npub: drawnField.counterpart,
fileName: drawnPdf.file.name pdfFileHash: fileHash,
} fileName: file.name
}) }
}) })
}) || []
)
}) })
.map((mark, index) => { .map((mark, index) => {
return { ...mark, id: index } return { ...mark, id: index }
@ -846,8 +849,8 @@ export const CreatePage = () => {
} }
} }
const onDrawFieldsChange = (pdfFiles: PdfFile[]) => { const onDrawFieldsChange = (sigitFiles: SigitFile[]) => {
setDrawnPdfs(pdfFiles) setDrawnFiles(sigitFiles)
} }
if (authUrl) { if (authUrl) {

View File

@ -38,8 +38,6 @@ import {
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import { DisplayMeta } from './internal/displayMeta' import { DisplayMeta } from './internal/displayMeta'
import styles from './style.module.scss' import styles from './style.module.scss'
import { PdfFile } from '../../types/drawing.ts'
import { convertToPdfFile } from '../../utils/pdf.ts'
import { CurrentUserMark, Mark } from '../../types/mark.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts'
import { getLastSignersSig } from '../../utils/sign.ts' import { getLastSignersSig } from '../../utils/sign.ts'
import { import {
@ -49,7 +47,11 @@ import {
updateMarks updateMarks
} from '../../utils' } from '../../utils'
import PdfMarking from '../../components/PDFView/PdfMarking.tsx' 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' import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
enum SignedStatus { enum SignedStatus {
Fully_Signed, Fully_Signed,
@ -76,7 +78,7 @@ export const SignPage = () => {
const [selectedFile, setSelectedFile] = useState<File | null>(null) const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
@ -402,7 +404,7 @@ export const SignPage = () => {
return return
} }
const files: { [filename: string]: PdfFile } = {} const files: { [filename: string]: SigitFile } = {}
const fileHashes: { [key: string]: string | null } = {} const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files).map((entry) => entry.name) const fileNames = Object.values(zip.files).map((entry) => entry.name)
@ -416,8 +418,7 @@ export const SignPage = () => {
) )
if (arrayBuffer) { if (arrayBuffer) {
files[fileName] = await convertToPdfFile(arrayBuffer, fileName) files[fileName] = await convertToSigitFile(arrayBuffer, fileName)
const hash = await getHash(arrayBuffer) const hash = await getHash(arrayBuffer)
if (hash) { if (hash) {
fileHashes[fileName] = hash fileHashes[fileName] = hash
@ -462,7 +463,7 @@ export const SignPage = () => {
const zip = await loadZip(decryptedZipFile) const zip = await loadZip(decryptedZipFile)
if (!zip) return if (!zip) return
const files: { [filename: string]: PdfFile } = {} const files: { [filename: string]: SigitFile } = {}
const fileHashes: { [key: string]: string | null } = {} const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files) const fileNames = Object.values(zip.files)
.filter((entry) => entry.name.startsWith('files/') && !entry.dir) .filter((entry) => entry.name.startsWith('files/') && !entry.dir)
@ -479,7 +480,7 @@ export const SignPage = () => {
) )
if (arrayBuffer) { if (arrayBuffer) {
files[fileName] = await convertToPdfFile(arrayBuffer, fileName) files[fileName] = await convertToSigitFile(arrayBuffer, fileName)
const hash = await getHash(arrayBuffer) const hash = await getHash(arrayBuffer)
if (hash) { if (hash) {
@ -764,8 +765,8 @@ export const SignPage = () => {
zip.file('meta.json', stringifiedMeta) zip.file('meta.json', stringifiedMeta)
for (const [fileName, pdf] of Object.entries(files)) { for (const [fileName, file] of Object.entries(files)) {
zip.file(`files/${fileName}`, await pdf.file.arrayBuffer()) zip.file(`files/${fileName}`, await file.arrayBuffer())
} }
const arrayBuffer = await zip const arrayBuffer = await zip
@ -802,8 +803,8 @@ export const SignPage = () => {
zip.file('meta.json', stringifiedMeta) zip.file('meta.json', stringifiedMeta)
for (const [fileName, pdf] of Object.entries(files)) { for (const [fileName, file] of Object.entries(files)) {
zip.file(`files/${fileName}`, await pdf.file.arrayBuffer()) zip.file(`files/${fileName}`, await file.arrayBuffer())
} }
const arrayBuffer = await zip const arrayBuffer = await zip

View File

@ -34,11 +34,11 @@ import { UserAvatar } from '../../../components/UserAvatar'
import { MetadataController } from '../../../controllers' import { MetadataController } from '../../../controllers'
import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils' import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils'
import styles from '../style.module.scss' import styles from '../style.module.scss'
import { PdfFile } from '../../../types/drawing.ts' import { SigitFile } from '../../../utils/file'
type DisplayMetaProps = { type DisplayMetaProps = {
meta: Meta meta: Meta
files: { [filename: string]: PdfFile } files: { [fileName: string]: SigitFile }
submittedBy: string submittedBy: string
signers: `npub1${string}`[] signers: `npub1${string}`[]
viewers: `npub1${string}`[] viewers: `npub1${string}`[]
@ -143,12 +143,9 @@ export const DisplayMeta = ({
}) })
}, [users, submittedBy, metadata]) }, [users, submittedBy, metadata])
const downloadFile = async (filename: string) => { const downloadFile = async (fileName: string) => {
const arrayBuffer = await files[filename].file.arrayBuffer() const file = files[fileName]
if (!arrayBuffer) return saveAs(file)
const blob = new Blob([arrayBuffer])
saveAs(blob, filename)
} }
return ( return (

View File

@ -2,8 +2,6 @@
.container { .container {
color: $text-color; color: $text-color;
//width: 550px;
//max-width: 550px;
.inputBlock { .inputBlock {
position: relative; position: relative;
@ -67,7 +65,7 @@
//z-index: 200; //z-index: 200;
} }
.fixedBottomForm input[type="text"] { .fixedBottomForm input[type='text'] {
width: 80%; width: 80%;
padding: 10px; padding: 10px;
font-size: 16px; font-size: 16px;

View File

@ -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 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'
@ -26,11 +26,9 @@ import {
import styles from './style.module.scss' import styles from './style.module.scss'
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 { import {
addMarks, addMarks,
convertToPdfBlob, convertToPdfBlob,
convertToPdfFile,
groupMarksByFileNamePage, groupMarksByFileNamePage,
inPx inPx
} from '../../utils/pdf.ts' } from '../../utils/pdf.ts'
@ -49,6 +47,9 @@ import FileList from '../../components/FileList'
import { CurrentUserFile } from '../../types/file.ts' import { CurrentUserFile } from '../../types/file.ts'
import { Mark } from '../../types/mark.ts' import { Mark } from '../../types/mark.ts'
import React from 'react' 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 { interface PdfViewProps {
files: CurrentUserFile[] files: CurrentUserFile[]
@ -67,71 +68,71 @@ const SlimPdfView = ({
useEffect(() => { useEffect(() => {
if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { if (currentFile !== null && !!pdfRefs.current[currentFile.id]) {
pdfRefs.current[currentFile.id]?.scrollIntoView({ pdfRefs.current[currentFile.id]?.scrollIntoView({
behavior: 'smooth', behavior: 'smooth'
block: 'end'
}) })
} }
}, [currentFile]) }, [currentFile])
return ( return (
<div className={styles.view}> <div className="files-wrapper">
{files.map((currentUserFile, i) => { {files.map((currentUserFile, i) => {
const { hash, filename, pdfFile, id } = currentUserFile const { hash, file, id } = currentUserFile
const signatureEvents = Object.keys(parsedSignatureEvents) const signatureEvents = Object.keys(parsedSignatureEvents)
if (!hash) return if (!hash) return
return ( return (
<React.Fragment key={filename}> <React.Fragment key={file.name}>
<div <div
id={filename} id={file.name}
ref={(el) => (pdfRefs.current[id] = el)} ref={(el) => (pdfRefs.current[id] = el)}
className={styles.fileWrapper} className="file-wrapper"
> >
{pdfFile.pages.map((page, i) => { {file.isPdf &&
const marks: Mark[] = [] file.pages?.map((page, i) => {
const marks: Mark[] = []
signatureEvents.forEach((e) => { signatureEvents.forEach((e) => {
const m = parsedSignatureEvents[ const m = parsedSignatureEvents[
e as `npub1${string}` e as `npub1${string}`
].parsedContent?.marks.filter( ].parsedContent?.marks.filter(
(m) => m.pdfFileHash == hash && m.location.page == i (m) => m.pdfFileHash == hash && m.location.page == i
)
if (m) {
marks.push(...m)
}
})
return (
<div className="image-wrapper" 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>
) )
if (m) { })}
marks.push(...m) {file.isImage && (
} <img
}) className="file-image"
return ( src={file.objectUrl}
<div className={styles.imageWrapper} key={i}> alt={file.name}
<img draggable="false" src={page.image} /> />
{marks.map((m) => { )}
return ( {!(file.isPdf || file.isImage) && (
<div <ExtensionFileBox extension={file.extension} />
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> </div>
{i < files.length - 1 && <FileDivider />}
{i < files.length - 1 && (
<Divider
sx={{
fontSize: '12px',
color: 'rgba(0,0,0,0.15)'
}}
>
File Separator
</Divider>
)}
</React.Fragment> </React.Fragment>
) )
})} })}
@ -171,7 +172,7 @@ export const VerifyPage = () => {
const [currentFileHashes, setCurrentFileHashes] = useState<{ const [currentFileHashes, setCurrentFileHashes] = useState<{
[key: string]: string | null [key: string]: string | null
}>({}) }>({})
const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null) const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
const [signatureFileHashes, setSignatureFileHashes] = useState<{ const [signatureFileHashes, setSignatureFileHashes] = useState<{
[key: string]: string [key: string]: string
@ -230,7 +231,7 @@ export const VerifyPage = () => {
if (!zip) return if (!zip) return
const files: { [filename: string]: PdfFile } = {} const files: { [fileName: string]: SigitFile } = {}
const fileHashes: { [key: string]: string | null } = {} const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files).map( const fileNames = Object.values(zip.files).map(
(entry) => entry.name (entry) => entry.name
@ -246,7 +247,7 @@ export const VerifyPage = () => {
) )
if (arrayBuffer) { if (arrayBuffer) {
files[fileName] = await convertToPdfFile( files[fileName] = await convertToSigitFile(
arrayBuffer, arrayBuffer,
fileName! fileName!
) )
@ -423,10 +424,15 @@ export const VerifyPage = () => {
const marks = extractMarksFromSignedMeta(updatedMeta) const marks = extractMarksFromSignedMeta(updatedMeta)
const marksByPage = groupMarksByFileNamePage(marks) const marksByPage = groupMarksByFileNamePage(marks)
for (const [fileName, pdf] of Object.entries(files)) { for (const [fileName, file] of Object.entries(files)) {
const pages = await addMarks(pdf.file, marksByPage[fileName]) if (file.isPdf) {
const blob = await convertToPdfBlob(pages) // Draw marks into PDF file and generate a brand new blob
zip.file(`files/${fileName}`, blob) const pages = await addMarks(file, marksByPage[fileName])
const blob = await convertToPdfBlob(pages)
zip.file(`files/${fileName}`, blob)
} else {
zip.file(`files/${fileName}`, file)
}
} }
const arrayBuffer = await zip const arrayBuffer = await zip

View File

@ -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 { .mark {
position: absolute; position: absolute;

View File

@ -8,12 +8,6 @@ export interface MouseState {
} }
} }
export interface PdfFile {
file: File
pages: PdfPage[]
expanded?: boolean
}
export interface PdfPage { export interface PdfPage {
image: string image: string
drawnFields: DrawnField[] drawnFields: DrawnField[]

View File

@ -1,9 +1,8 @@
import { PdfFile } from './drawing.ts' import { SigitFile } from '../utils/file'
export interface CurrentUserFile { export interface CurrentUserFile {
id: number id: number
pdfFile: PdfFile file: SigitFile
filename: string
hash?: string hash?: string
isHashValid: boolean isHashValid: boolean
} }

View File

@ -26,3 +26,94 @@ export const DEFAULT_LOOK_UP_RELAY_LIST = [
'wss://user.kindpag.es', 'wss://user.kindpag.es',
'wss://purplepag.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
])

View File

@ -1,24 +1,139 @@
import { Meta } from '../types' import { Meta } from '../types'
import { PdfPage } from '../types/drawing.ts'
import { MOST_COMMON_MEDIA_TYPES } from './const.ts'
import { extractMarksFromSignedMeta } from './mark.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 JSZip from 'jszip'
import { PdfFile } from '../types/drawing.ts'
const getZipWithFiles = async ( export const getZipWithFiles = async (
meta: Meta, meta: Meta,
files: { [filename: string]: PdfFile } files: { [filename: string]: SigitFile }
): Promise<JSZip> => { ): Promise<JSZip> => {
const zip = new JSZip() const zip = new JSZip()
const marks = extractMarksFromSignedMeta(meta) const marks = extractMarksFromSignedMeta(meta)
const marksByFileNamePage = groupMarksByFileNamePage(marks) const marksByFileNamePage = groupMarksByFileNamePage(marks)
for (const [fileName, pdf] of Object.entries(files)) { for (const [fileName, file] of Object.entries(files)) {
const pages = await addMarks(pdf.file, marksByFileNamePage[fileName]) if (file.isPdf) {
const blob = await convertToPdfBlob(pages) // Handle PDF Files
zip.file(`files/${fileName}`, blob) const pages = await addMarks(file, marksByFileNamePage[fileName])
const blob = await convertToPdfBlob(pages)
zip.file(`files/${fileName}`, blob)
} else {
// Handle other files
zip.file(`files/${fileName}`, file)
}
} }
return zip 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
isPdf: boolean
isImage: boolean
pages?: PdfPage[]
objectUrl?: string
constructor(file: File) {
super([file], file.name, { type: file.type })
this.isPdf = isPdf(this)
this.isImage = isImage(this)
this.extension = extractFileExtension(this.name)
}
async process() {
if (this.isPdf) this.pages = await pdfToImages(await this.arrayBuffer())
if (this.isImage) this.objectUrl = URL.createObjectURL(this)
}
}
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<SigitFile> => {
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)
}
export const isImage = (file: File) => {
const validImageMediaTypes = [
'image/png',
'image/jpeg',
'image/jpg',
'image/gif',
'image/svg+xml',
'image/bmp',
'image/x-icon'
]
return validImageMediaTypes.includes(file.type.toLowerCase())
}

View File

@ -2,6 +2,7 @@ import { CreateSignatureEventContent, Meta } from '../types'
import { fromUnixTimestamp, parseJson } from '.' import { fromUnixTimestamp, parseJson } from '.'
import { Event, verifyEvent } from 'nostr-tools' import { Event, verifyEvent } from 'nostr-tools'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { extractFileExtensions } from './file'
export enum SignStatus { export enum SignStatus {
Signed = 'Signed', 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()
}

View File

@ -1,4 +1,4 @@
import { PdfFile, PdfPage } from '../types/drawing.ts' import { PdfPage } from '../types/drawing.ts'
import * as PDFJS from 'pdfjs-dist' import * as PDFJS from 'pdfjs-dist'
import { PDFDocument } from 'pdf-lib' import { PDFDocument } from 'pdf-lib'
import { Mark } from '../types/mark.ts' 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 * Scale between the PDF page's natural size and rendered size
* @constant {number} * @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 * 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 * correlate font size used at the time of filling in / drawing on the PDF
@ -20,58 +20,28 @@ const SCALE: number = 3
* This should be fixed going forward. * This should be fixed going forward.
* Switching to PDF-Lib will most likely make this problem redundant. * 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. * Current font type used when generating a PDF.
*/ */
const FONT_TYPE: string = 'Arial' export 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
*/
const toFile = (arrayBuffer: ArrayBuffer, fileName: string): File => {
const blob = new Blob([arrayBuffer], { type: 'application/pdf' })
return new File([blob], fileName, { type: 'application/pdf' })
}
/**
* 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<PdfFile> => {
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<PdfFile[]> => {
return Promise.all(selectedFiles.filter(isPdf).map(toPdfFile))
}
/** /**
* A utility that transforms a drawing coordinate number into a CSS-compatible string * A utility that transforms a drawing coordinate number into a CSS-compatible string
* @param coordinate * @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 * A utility that checks if a given file is of the pdf type
* @param file * @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 * Reads the pdf file binaries
*/ */
const readPdf = (file: File): Promise<string | ArrayBuffer> => { export 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()
@ -99,7 +69,9 @@ const readPdf = (file: File): Promise<string | ArrayBuffer> => {
* Converts pdf to the images * Converts pdf to the images
* @param data pdf file bytes * @param data pdf file bytes
*/ */
const pdfToImages = async (data: string | ArrayBuffer): Promise<PdfPage[]> => { export 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')
@ -129,7 +101,7 @@ const pdfToImages = async (data: string | ArrayBuffer): Promise<PdfPage[]> => {
* Returns an array of encoded images where each image is a representation * Returns an array of encoded images where each image is a representation
* of a PDF page with completed and signed marks from all users * of a PDF page with completed and signed marks from all users
*/ */
const addMarks = async ( export const addMarks = async (
file: File, file: File,
marksPerPage: { [key: string]: Mark[] } marksPerPage: { [key: string]: Mark[] }
) => { ) => {
@ -159,7 +131,7 @@ const addMarks = async (
/** /**
* Utility to scale mark in line with the PDF-to-PNG scale * 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 const { location } = mark
return { return {
...mark, ...mark,
@ -177,14 +149,14 @@ const scaleMark = (mark: Mark): Mark => {
* Utility to check if a Mark has value * Utility to check if a Mark has value
* @param mark * @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 * Draws a Mark on a Canvas representation of a PDF Page
* @param mark to be drawn * @param mark to be drawn
* @param ctx a Canvas representation of a specific PDF Page * @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 const { location } = mark
ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE
@ -199,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 * Takes an array of encoded PDF pages and returns a blob that is a complete PDF file
* @param markedPdfPages * @param markedPdfPages
*/ */
const convertToPdfBlob = async (markedPdfPages: string[]): Promise<Blob> => { export const convertToPdfBlob = async (
markedPdfPages: string[]
): Promise<Blob> => {
const pdfDoc = await PDFDocument.create() const pdfDoc = await PDFDocument.create()
for (const page of markedPdfPages) { for (const page of markedPdfPages) {
@ -217,30 +191,17 @@ const convertToPdfBlob = async (markedPdfPages: string[]): Promise<Blob> => {
return new Blob([pdfBytes], { type: 'application/pdf' }) 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<PdfFile> => {
const file = toFile(arrayBuffer, fileName)
return toPdfFile(file)
}
/** /**
* @param marks - an array of Marks * @param marks - an array of Marks
* @function hasValue removes any Mark without a property * @function hasValue removes any Mark without a property
* @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 groupMarksByFileNamePage = (marks: Mark[]) => { export const groupMarksByFileNamePage = (marks: Mark[]) => {
return marks return marks
.filter(hasValue) .filter(hasValue)
.map(scaleMark) .map(scaleMark)
.reduce<{ [filename: string]: { [page: number]: Mark[] } }>(byPage, {}) .reduce<{ [fileName: string]: { [page: number]: Mark[] } }>(byPage, {})
} }
/** /**
@ -251,30 +212,19 @@ const groupMarksByFileNamePage = (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 = ( export const byPage = (
obj: { [filename: string]: { [page: number]: Mark[] } }, obj: { [filename: string]: { [page: number]: Mark[] } },
mark: Mark mark: Mark
) => { ) => {
const filename = mark.fileName const fileName = mark.fileName
const pageNumber = mark.location.page const pageNumber = mark.location.page
const pages = obj[filename] ?? {} const pages = obj[fileName] ?? {}
const marks = pages[pageNumber] ?? [] const marks = pages[pageNumber] ?? []
return { return {
...obj, ...obj,
[filename]: { [fileName]: {
...pages, ...pages,
[pageNumber]: [...marks, mark] [pageNumber]: [...marks, mark]
} }
} }
} }
export {
toFile,
toPdfFile,
toPdfFiles,
inPx,
convertToPdfFile,
addMarks,
convertToPdfBlob,
groupMarksByFileNamePage
}

View File

@ -1,5 +1,5 @@
import { PdfFile } from '../types/drawing.ts'
import { CurrentUserFile } from '../types/file.ts' import { CurrentUserFile } from '../types/file.ts'
import { SigitFile } from './file.ts'
export const compareObjects = ( export const compareObjects = (
obj1: object | null | undefined, obj1: object | null | undefined,
@ -75,13 +75,13 @@ export const timeout = (ms: number = 60000) => {
* @param creatorFileHashes * @param creatorFileHashes
*/ */
export const getCurrentUserFiles = ( export const getCurrentUserFiles = (
files: { [filename: string]: PdfFile }, files: { [filename: string]: SigitFile },
fileHashes: { [key: string]: string | null }, fileHashes: { [key: string]: string | null },
creatorFileHashes: { [key: string]: string } creatorFileHashes: { [key: string]: string }
): CurrentUserFile[] => { ): CurrentUserFile[] => {
return Object.entries(files).map(([filename, pdfFile], index) => { return Object.entries(files).map(([filename, file], index) => {
return { return {
pdfFile, file,
filename, filename,
id: index + 1, id: index + 1,
...(!!fileHashes[filename] && { hash: fileHashes[filename]! }), ...(!!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 <Item, FulfilledItem = Item>(
array: Item[],
cb: (arg: Item) => Promise<FulfilledItem>
) => {
// 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<NonNullable<FulfilledItem>[]>((acc, result) => {
if (result.status === 'fulfilled') {
const value = result.value
if (value) {
acc.push(value)
}
}
return acc
}, [])
}