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