chore(git): merge pull request #163 from 138-other-file-types into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m15s
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m15s
Reviewed-on: #163 Reviewed-by: s <sabir@4gl.io>
This commit is contained in:
commit
f4aefbb200
70
src/App.scss
70
src/App.scss
@ -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;
|
||||||
|
}
|
||||||
|
@ -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} /> —
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.itemActions}>
|
<div className={styles.itemActions}>
|
||||||
<Tooltip title="Duplicate" arrow placement="top" disableInteractive>
|
<Tooltip title="Duplicate" arrow placement="top" disableInteractive>
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
6
src/components/ExtensionFileBox.tsx
Normal file
6
src/components/ExtensionFileBox.tsx
Normal 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>
|
||||||
|
)
|
12
src/components/FileDivider.tsx
Normal file
12
src/components/FileDivider.tsx
Normal 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>
|
||||||
|
)
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
@ -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,7 +32,8 @@ 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 file.pages?.map((page, i) => {
|
||||||
return (
|
return (
|
||||||
<PdfPageItem
|
<PdfPageItem
|
||||||
page={page}
|
page={page}
|
||||||
@ -44,6 +46,11 @@ const PdfItem = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
} 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
|
||||||
|
@ -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,9 +133,7 @@ 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 && (
|
||||||
<div className={styles.pdfView}>
|
|
||||||
<PdfView
|
<PdfView
|
||||||
currentFile={currentFile}
|
currentFile={currentFile}
|
||||||
files={files}
|
files={files}
|
||||||
@ -146,9 +143,7 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
currentUserMarks={currentUserMarks}
|
currentUserMarks={currentUserMarks}
|
||||||
otherUserMarks={otherUserMarks}
|
otherUserMarks={otherUserMarks}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</StickySideColumns>
|
</StickySideColumns>
|
||||||
{selectedMark !== null && (
|
{selectedMark !== null && (
|
||||||
<MarkFormField
|
<MarkFormField
|
||||||
|
@ -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
|
||||||
|
@ -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 (
|
||||||
|
<React.Fragment key={index}>
|
||||||
<div
|
<div
|
||||||
id={pdfFile.file.name}
|
id={file.name}
|
||||||
|
className="file-wrapper"
|
||||||
ref={(el) => (pdfRefs.current[id] = el)}
|
ref={(el) => (pdfRefs.current[id] = el)}
|
||||||
key={index}
|
|
||||||
>
|
>
|
||||||
<PdfItem
|
<PdfItem
|
||||||
pdfFile={pdfFile}
|
file={file}
|
||||||
currentUserMarks={filterByFile(currentUserMarks, hash)}
|
currentUserMarks={filterByFile(currentUserMarks, hash)}
|
||||||
selectedMark={selectedMark}
|
selectedMark={selectedMark}
|
||||||
handleMarkClick={handleMarkClick}
|
handleMarkClick={handleMarkClick}
|
||||||
selectedMarkValue={selectedMarkValue}
|
selectedMarkValue={selectedMarkValue}
|
||||||
otherUserMarks={filterMarksByFile(otherUserMarks, hash)}
|
otherUserMarks={filterMarksByFile(otherUserMarks, hash)}
|
||||||
/>
|
/>
|
||||||
{isNotLastPdfFile(index, arr) && <Divider>File Separator</Divider>}
|
|
||||||
</div>
|
</div>
|
||||||
|
{isNotLastPdfFile(index, arr) && <FileDivider />}
|
||||||
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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!')
|
||||||
|
@ -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,10 +508,11 @@ 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 (
|
||||||
|
file.pages?.flatMap((page, index) => {
|
||||||
return page.drawnFields.map((drawnField) => {
|
return page.drawnFields.map((drawnField) => {
|
||||||
return {
|
return {
|
||||||
type: drawnField.type,
|
type: drawnField.type,
|
||||||
@ -523,10 +525,11 @@ export const CreatePage = () => {
|
|||||||
},
|
},
|
||||||
npub: drawnField.counterpart,
|
npub: drawnField.counterpart,
|
||||||
pdfFileHash: fileHash,
|
pdfFileHash: fileHash,
|
||||||
fileName: drawnPdf.file.name
|
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) {
|
||||||
|
@ -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
|
||||||
|
@ -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 (
|
||||||
|
@ -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;
|
||||||
|
@ -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,25 +68,25 @@ 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 &&
|
||||||
|
file.pages?.map((page, i) => {
|
||||||
const marks: Mark[] = []
|
const marks: Mark[] = []
|
||||||
|
|
||||||
signatureEvents.forEach((e) => {
|
signatureEvents.forEach((e) => {
|
||||||
@ -99,7 +100,7 @@ const SlimPdfView = ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<div className={styles.imageWrapper} key={i}>
|
<div className="image-wrapper" key={i}>
|
||||||
<img draggable="false" src={page.image} />
|
<img draggable="false" src={page.image} />
|
||||||
{marks.map((m) => {
|
{marks.map((m) => {
|
||||||
return (
|
return (
|
||||||
@ -120,18 +121,18 @@ const SlimPdfView = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
{file.isImage && (
|
||||||
|
<img
|
||||||
{i < files.length - 1 && (
|
className="file-image"
|
||||||
<Divider
|
src={file.objectUrl}
|
||||||
sx={{
|
alt={file.name}
|
||||||
fontSize: '12px',
|
/>
|
||||||
color: 'rgba(0,0,0,0.15)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
File Separator
|
|
||||||
</Divider>
|
|
||||||
)}
|
)}
|
||||||
|
{!(file.isPdf || file.isImage) && (
|
||||||
|
<ExtensionFileBox extension={file.extension} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{i < files.length - 1 && <FileDivider />}
|
||||||
</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) {
|
||||||
|
// Draw marks into PDF file and generate a brand new blob
|
||||||
|
const pages = await addMarks(file, marksByPage[fileName])
|
||||||
const blob = await convertToPdfBlob(pages)
|
const blob = await convertToPdfBlob(pages)
|
||||||
zip.file(`files/${fileName}`, blob)
|
zip.file(`files/${fileName}`, blob)
|
||||||
|
} else {
|
||||||
|
zip.file(`files/${fileName}`, file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrayBuffer = await zip
|
const arrayBuffer = await zip
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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[]
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
])
|
||||||
|
@ -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) {
|
||||||
|
// Handle PDF Files
|
||||||
|
const pages = await addMarks(file, marksByFileNamePage[fileName])
|
||||||
const blob = await convertToPdfBlob(pages)
|
const blob = await convertToPdfBlob(pages)
|
||||||
zip.file(`files/${fileName}`, blob)
|
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())
|
||||||
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user