diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml
index 04f4fd7..3e68d43 100644
--- a/.gitea/workflows/release-staging.yaml
+++ b/.gitea/workflows/release-staging.yaml
@@ -18,7 +18,7 @@ jobs:
node-version: 18
- name: Audit
- run: npm audit
+ run: npm audit --omit=dev
- name: Install Dependencies
run: npm ci
diff --git a/.gitea/workflows/staging-pull-request.yaml b/.gitea/workflows/staging-pull-request.yaml
index 2bebcd4..cba8164 100644
--- a/.gitea/workflows/staging-pull-request.yaml
+++ b/.gitea/workflows/staging-pull-request.yaml
@@ -19,7 +19,7 @@ jobs:
node-version: 18
- name: Audit
- run: npm audit
+ run: npm audit --omit=dev
- name: Install Dependencies
run: npm ci
diff --git a/package-lock.json b/package-lock.json
index ef46577..2bcd952 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,13 +1,14 @@
{
- "name": "web",
+ "name": "sigit",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "web",
+ "name": "sigit",
"version": "0.0.0",
"hasInstallScript": true,
+ "license": "AGPL-3.0-or-later ",
"dependencies": {
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.0",
@@ -33,13 +34,15 @@
"nostr-tools": "2.7.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^4.4.168",
+ "rdndmb-html5-to-touch": "^8.0.3",
"react": "^18.2.0",
- "react-dnd": "16.0.1",
- "react-dnd-html5-backend": "16.0.1",
+ "react-dnd": "^16.0.1",
+ "react-dnd-multi-backend": "^8.0.3",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-redux": "9.1.0",
"react-router-dom": "6.22.1",
+ "react-singleton-hook": "^4.0.1",
"react-toastify": "10.0.4",
"redux": "5.0.1",
"tseep": "1.2.1"
@@ -3265,6 +3268,19 @@
"@babel/runtime": "^7.9.2"
}
},
+ "node_modules/dnd-multi-backend": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/dnd-multi-backend/-/dnd-multi-backend-8.0.3.tgz",
+ "integrity": "sha512-yFFARotr+OEJk787Fsj+V52pi6j7+Pt/CRp3IR2Ai3fnxA/z6J54T7+gxkXzXu4cvxTNE7NiBzzAaJ2f7JjFTw==",
+ "license": "MIT",
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/LouisBrunner"
+ },
+ "peerDependencies": {
+ "dnd-core": "^16.0.1"
+ }
+ },
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -5685,6 +5701,21 @@
}
]
},
+ "node_modules/rdndmb-html5-to-touch": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/rdndmb-html5-to-touch/-/rdndmb-html5-to-touch-8.0.3.tgz",
+ "integrity": "sha512-VfIbLjlL9NAnZzc2M5fGPCNkDyK12+ahgILGO5RjS7jkgUlxwB0c/XvxVQNfY/2ocg7isTY/G7tqxJk5fSTZAA==",
+ "license": "MIT",
+ "dependencies": {
+ "dnd-multi-backend": "^8.0.3",
+ "react-dnd-html5-backend": "^16.0.1",
+ "react-dnd-touch-backend": "^16.0.1"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/LouisBrunner"
+ }
+ },
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -5700,6 +5731,7 @@
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
"integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
+ "license": "MIT",
"dependencies": {
"@react-dnd/invariant": "^4.0.1",
"@react-dnd/shallowequal": "^4.0.1",
@@ -5729,10 +5761,55 @@
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
"integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
+ "license": "MIT",
"dependencies": {
"dnd-core": "^16.0.1"
}
},
+ "node_modules/react-dnd-multi-backend": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/react-dnd-multi-backend/-/react-dnd-multi-backend-8.0.3.tgz",
+ "integrity": "sha512-IwH7Mf6R05KIFohX0hHMTluoAvuUD8SO15KCD+9fY0nJ4nc1FGCMCSyMZw8R1XNStKp+JnNg3ZMtiaf5DebSUg==",
+ "license": "MIT",
+ "dependencies": {
+ "dnd-multi-backend": "^8.0.3",
+ "react-dnd-preview": "^8.0.3"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/LouisBrunner"
+ },
+ "peerDependencies": {
+ "dnd-core": "^16.0.1",
+ "react": "^16.14.0 || ^17.0.2 || ^18.0.0",
+ "react-dnd": "^16.0.1",
+ "react-dom": "^16.14.0 || ^17.0.2 || ^18.0.0"
+ }
+ },
+ "node_modules/react-dnd-preview": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/react-dnd-preview/-/react-dnd-preview-8.0.3.tgz",
+ "integrity": "sha512-s69Ro47QYDthDhj73iQ0VioMCjtlZ1AytKBDkQaHKm5DTjA8D2bIaFKCBQd330QEW0SIzqLJrZGCSlIY2xraJg==",
+ "license": "MIT",
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/LouisBrunner"
+ },
+ "peerDependencies": {
+ "react": "^16.14.0 || ^17.0.2 || ^18.0.0",
+ "react-dnd": "^16.0.1"
+ }
+ },
+ "node_modules/react-dnd-touch-backend": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz",
+ "integrity": "sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-dnd/invariant": "^4.0.1",
+ "dnd-core": "^16.0.1"
+ }
+ },
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@@ -5832,6 +5909,23 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/react-singleton-hook": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/react-singleton-hook/-/react-singleton-hook-4.0.1.tgz",
+ "integrity": "sha512-fWuk8VxcZPChrkQasDLM8pgd/7kyi+Cr/5FfCiD99FicjEru+JmtEZNnN4lJ8Z7KbqAST5CYPlpz6lmNsZFGNw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-toastify": {
"version": "10.0.4",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.4.tgz",
diff --git a/package.json b/package.json
index c835018..cf7ae91 100644
--- a/package.json
+++ b/package.json
@@ -44,13 +44,15 @@
"nostr-tools": "2.7.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^4.4.168",
+ "rdndmb-html5-to-touch": "^8.0.3",
"react": "^18.2.0",
- "react-dnd": "16.0.1",
- "react-dnd-html5-backend": "16.0.1",
+ "react-dnd": "^16.0.1",
+ "react-dnd-multi-backend": "^8.0.3",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-redux": "9.1.0",
"react-router-dom": "6.22.1",
+ "react-singleton-hook": "^4.0.1",
"react-toastify": "10.0.4",
"redux": "5.0.1",
"tseep": "1.2.1"
@@ -82,4 +84,4 @@
],
"*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}": "npm run formatter:staged"
}
-}
\ No newline at end of file
+}
diff --git a/src/App.scss b/src/App.scss
index 6724890..4d95be6 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -41,6 +41,7 @@ p {
body {
color: $text-color;
+ background: $body-background-color;
font-family: $font-familiy;
letter-spacing: $letter-spacing;
font-size: $body-font-size;
@@ -69,3 +70,96 @@ a {
input {
font-family: inherit;
}
+
+ul {
+ list-style-type: none; /* Removes bullet points */
+ margin: 0; /* Removes default margin */
+ padding: 0; /* Removes default padding */
+}
+
+li {
+ list-style-type: none; /* Removes the bullets */
+ margin: 0; /* Removes any default margin */
+ padding: 0; /* Removes any default padding */
+}
+
+// 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;
+
+ > img {
+ display: block;
+ width: 100%;
+ height: auto;
+ 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 */
+}
+
+// Consistent styling for every file mark
+// Reverts some of the design defaults for font
+.file-mark {
+ font-family: Arial;
+ font-size: 16px;
+ font-weight: normal;
+ color: black;
+ letter-spacing: normal;
+ border: 1px solid transparent;
+
+ scroll-margin-top: $header-height + $body-vertical-padding;
+}
+
+[data-dev='true'] {
+ .image-wrapper {
+ // outline: 1px solid #ccc; /* Optional: for visual debugging */
+ background-color: #e0f7fa; /* Optional: for visual debugging */
+ }
+}
+
+.extension-file-box {
+ border-radius: 4px;
+ background: rgba(255, 255, 255, 0.5);
+ height: 100px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ color: rgba(0, 0, 0, 0.25);
+ font-size: 14px;
+}
diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx
index 92dc01d..473a942 100644
--- a/src/components/DisplaySigit/index.tsx
+++ b/src/components/DisplaySigit/index.tsx
@@ -10,7 +10,8 @@ import {
faCalendar,
faCopy,
faEye,
- faFile
+ faFile,
+ faFileCircleExclamation
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { UserAvatarGroup } from '../UserAvatarGroup'
@@ -20,6 +21,7 @@ import { TooltipChild } from '../TooltipChild'
import { getExtensionIconLabel } from '../getExtensionIconLabel'
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
import { useSigitMeta } from '../../hooks/useSigitMeta'
+import { extractFileExtensions } from '../../utils/file'
type SigitProps = {
meta: Meta
@@ -27,23 +29,18 @@ type SigitProps = {
}
export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
- const {
- title,
- createdAt,
- submittedBy,
- signers,
- signedStatus,
- fileExtensions,
- isValid
- } = parsedMeta
+ const { title, createdAt, submittedBy, signers, signedStatus, isValid } =
+ parsedMeta
- const { signersStatus } = useSigitMeta(meta)
+ const { signersStatus, fileHashes } = useSigitMeta(meta)
const profiles = useSigitProfiles([
...(submittedBy ? [submittedBy] : []),
...signers
])
+ const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
+
return (
{
{signedStatus}
- {fileExtensions.length > 0 ? (
+ {extensions.length > 0 ? (
- {fileExtensions.length > 1 ? (
+ {!isSame ? (
<>
Multiple File Types
>
) : (
- getExtensionIconLabel(fileExtensions[0])
+ getExtensionIconLabel(extensions[0])
)}
- ) : null}
+ ) : (
+ <>
+ —
+ >
+ )}
diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx
index a3d43ae..7efae8b 100644
--- a/src/components/DrawPDFFields/index.tsx
+++ b/src/components/DrawPDFFields/index.tsx
@@ -1,104 +1,105 @@
import { Close } from '@mui/icons-material'
import {
- Box,
- CircularProgress,
- Divider,
FormControl,
InputLabel,
+ ListItemIcon,
+ ListItemText,
MenuItem,
Select
} 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'
-PDFJS.GlobalWorkerOptions.workerSrc = new URL(
- 'pdfjs-dist/build/pdf.worker.min.mjs',
- import.meta.url
-).toString()
+import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils'
+import { getSigitFile, SigitFile } from '../../utils/file'
+import { FileDivider } from '../FileDivider'
+import { ExtensionFileBox } from '../ExtensionFileBox'
+import { inPx } from '../../utils/pdf'
+import { useScale } from '../../hooks/useScale'
+import { AvatarIconButton } from '../UserAvatarIconButton'
+import { LoadingSpinner } from '../LoadingSpinner'
+
+const DEFAULT_START_SIZE = {
+ width: 140,
+ height: 40
+} as const
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 { to, from } = useScale()
- const [pdfFiles, setPdfFiles] = useState([])
- const [parsingPdf, setParsingPdf] = useState(false)
+ const [sigitFiles, setSigitFiles] = useState([])
+ const [parsingPdf, setIsParsing] = useState(false)
const [mouseState, setMouseState] = useState({
clicked: false
})
+ const [activeDrawField, setActiveDrawField] = useState()
+
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)
+ window.addEventListener('pointerup', handlePointerUp)
+ window.addEventListener('pointercancel', handlePointerUp)
return () => {
- // window.removeEventListener('mousedown', onMouseDown);
- window.removeEventListener('mouseup', onMouseUp)
+ window.removeEventListener('pointerup', handlePointerUp)
+ window.removeEventListener('pointercancel', handlePointerUp)
}
}, [])
const refreshPdfFiles = () => {
- setPdfFiles([...pdfFiles])
+ setSigitFiles([...sigitFiles])
}
/**
- * Fired only when left click and mouse over pdf page
+ * Fired only on when left (primary pointer interaction) clicking page image
* Creates new drawnElement and pushes in the array
* It is re rendered and visible right away
*
- * @param event Mouse event
+ * @param event Pointer event
* @param page PdfPage where press happened
*/
- const onMouseDown = (
- event: React.MouseEvent,
- page: PdfPage
- ) => {
+ const handlePointerDown = (event: React.PointerEvent, page: PdfPage) => {
// Proceed only if left click
if (event.button !== 0) return
@@ -106,13 +107,13 @@ export const DrawPDFFields = (props: Props) => {
return
}
- const { mouseX, mouseY } = getMouseCoordinates(event)
+ const { x, y } = getPointerCoordinates(event)
const newField: DrawnField = {
- left: mouseX,
- top: mouseY,
- width: 0,
- height: 0,
+ left: to(page.width, x),
+ top: to(page.width, y),
+ width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width,
+ height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height,
counterpart: '',
type: selectedTool.identifier
}
@@ -129,9 +130,9 @@ export const DrawPDFFields = (props: Props) => {
/**
* Drawing is finished, resets all the variables used to draw
- * @param event Mouse event
+ * @param event Pointer event
*/
- const onMouseUp = () => {
+ const handlePointerUp = () => {
setMouseState((prev) => {
return {
...prev,
@@ -143,16 +144,13 @@ export const DrawPDFFields = (props: Props) => {
}
/**
- * After {@link onMouseDown} create an drawing element, this function gets called on every pixel moved
- * which alters the newly created drawing element, resizing it while mouse move
- * @param event Mouse event
+ * After {@link handlePointerDown} create an drawing element, this function gets called on every pixel moved
+ * which alters the newly created drawing element, resizing it while pointer moves
+ * @param event Pointer event
* @param page PdfPage where moving is happening
*/
- const onMouseMove = (
- event: React.MouseEvent,
- page: PdfPage
- ) => {
- if (mouseState.clicked && selectedTool) {
+ const handlePointerMove = (event: React.PointerEvent, page: PdfPage) => {
+ if (mouseState.clicked && selectedTool && event.pointerType === 'mouse') {
const lastElementIndex = page.drawnFields.length - 1
const lastDrawnField = page.drawnFields[lastElementIndex]
@@ -162,10 +160,10 @@ export const DrawPDFFields = (props: Props) => {
// to the page below (without releaseing mouse click)
if (!lastDrawnField) return
- const { mouseX, mouseY } = getMouseCoordinates(event)
+ const { x, y } = getPointerCoordinates(event)
- const width = mouseX - lastDrawnField.left
- const height = mouseY - lastDrawnField.top
+ const width = to(page.width, x) - lastDrawnField.left
+ const height = to(page.width, y) - lastDrawnField.top
lastDrawnField.width = width
lastDrawnField.height = height
@@ -180,54 +178,60 @@ export const DrawPDFFields = (props: Props) => {
/**
* Fired when event happens on the drawn element which will be moved
- * mouse coordinates relative to drawn element will be stored
+ * pointer coordinates relative to drawn element will be stored
* so when we start moving, offset can be calculated
- * mouseX - offsetX
- * mouseY - offsetY
+ * x - offsetX
+ * y - offsetY
*
- * @param event Mouse event
- * @param drawnField Which we are moving
+ * @param event Pointer event
+ * @param drawnFieldIndex Which we are moving
*/
- const onDrawnFieldMouseDown = (event: React.MouseEvent) => {
+ const handleDrawnFieldPointerDown = (
+ event: React.PointerEvent,
+ drawnFieldIndex: number
+ ) => {
event.stopPropagation()
// Proceed only if left click
if (event.button !== 0) return
- const drawingRectangleCoords = getMouseCoordinates(event)
+ const drawingRectangleCoords = getPointerCoordinates(event)
+ setActiveDrawField(drawnFieldIndex)
setMouseState({
dragging: true,
clicked: false,
coordsInWrapper: {
- mouseX: drawingRectangleCoords.mouseX,
- mouseY: drawingRectangleCoords.mouseY
+ x: drawingRectangleCoords.x,
+ y: drawingRectangleCoords.y
}
})
}
/**
- * Moves the drawnElement by the mouse position (mouse can grab anywhere on the drawn element)
- * @param event Mouse event
+ * Moves the drawnElement by the pointer position (pointer can grab anywhere on the drawn element)
+ * @param event Pointer event
* @param drawnField which we are moving
+ * @param pageWidth pdf value which is used to calculate scaled offset
*/
- const onDrawnFieldMouseMove = (
- event: React.MouseEvent,
- drawnField: DrawnField
+ const handleDrawnFieldPointerMove = (
+ event: React.PointerEvent,
+ drawnField: DrawnField,
+ pageWidth: number
) => {
if (mouseState.dragging) {
- const { mouseX, mouseY, rect } = getMouseCoordinates(
+ const { x, y, rect } = getPointerCoordinates(
event,
event.currentTarget.parentElement
)
const coordsOffset = mouseState.coordsInWrapper
if (coordsOffset) {
- let left = mouseX - coordsOffset.mouseX
- let top = mouseY - coordsOffset.mouseY
+ let left = to(pageWidth, x - coordsOffset.x)
+ let top = to(pageWidth, y - coordsOffset.y)
- const rightLimit = rect.width - drawnField.width - 3
- const bottomLimit = rect.height - drawnField.height - 3
+ const rightLimit = to(pageWidth, rect.width) - drawnField.width
+ const bottomLimit = to(pageWidth, rect.height) - drawnField.height
if (left < 0) left = 0
if (top < 0) top = 0
@@ -244,17 +248,18 @@ export const DrawPDFFields = (props: Props) => {
/**
* Fired when clicked on the resize handle, sets the state for a resize action
- * @param event Mouse event
- * @param drawnField which we are resizing
+ * @param event Pointer event
+ * @param drawnFieldIndex which we are resizing
*/
- const onResizeHandleMouseDown = (
- event: React.MouseEvent
+ const handleResizePointerDown = (
+ event: React.PointerEvent,
+ drawnFieldIndex: number
) => {
// Proceed only if left click
if (event.button !== 0) return
-
event.stopPropagation()
+ setActiveDrawField(drawnFieldIndex)
setMouseState({
resizing: true
})
@@ -262,15 +267,17 @@ export const DrawPDFFields = (props: Props) => {
/**
* Resizes the drawn element by the mouse position
- * @param event Mouse event
+ * @param event Pointer event
* @param drawnField which we are resizing
+ * @param pageWidth pdf value which is used to calculate scaled offset
*/
- const onResizeHandleMouseMove = (
- event: React.MouseEvent,
- drawnField: DrawnField
+ const handleResizePointerMove = (
+ event: React.PointerEvent,
+ drawnField: DrawnField,
+ pageWidth: number
) => {
if (mouseState.resizing) {
- const { mouseX, mouseY } = getMouseCoordinates(
+ const { x, y } = getPointerCoordinates(
event,
// currentTarget = span handle
// 1st parent = drawnField
@@ -278,8 +285,8 @@ export const DrawPDFFields = (props: Props) => {
event.currentTarget.parentElement?.parentElement
)
- const width = mouseX - drawnField.left
- const height = mouseY - drawnField.top
+ const width = to(pageWidth, x) - drawnField.left
+ const height = to(pageWidth, y) - drawnField.top
drawnField.width = width
drawnField.height = height
@@ -290,111 +297,137 @@ export const DrawPDFFields = (props: Props) => {
/**
* Removes the drawn element using the indexes in the params
- * @param event Mouse event
+ * @param event Pointer event
* @param pdfFileIndex pdf file index
* @param pdfPageIndex pdf page index
* @param drawnFileIndex drawn file index
*/
- const onRemoveHandleMouseDown = (
- event: React.MouseEvent,
+ const handleRemovePointerDown = (
+ event: React.PointerEvent,
pdfFileIndex: number,
pdfPageIndex: number,
drawnFileIndex: number
) => {
event.stopPropagation()
- pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice(
- drawnFileIndex,
- 1
- )
- }
-
- /**
- * Used to stop mouse click propagating to the parent elements
- * so select can work properly
- * @param event Mouse event
- */
- const onUserSelectHandleMouseDown = (
- event: React.MouseEvent
- ) => {
- event.stopPropagation()
- }
-
- /**
- * Gets the mouse coordinates relative to a element in the `event` param
- * @param event MouseEvent
- * @param customTarget mouse coordinates relative to this element, if not provided
- * event.target will be used
- */
- const getMouseCoordinates = (
- event: React.MouseEvent,
- customTarget?: HTMLElement | null
- ) => {
- const target = customTarget ? customTarget : event.currentTarget
- const rect = target.getBoundingClientRect()
- const mouseX = event.clientX - rect.left //x position within the element.
- const mouseY = event.clientY - rect.top //y position within the element.
-
- return {
- mouseX,
- mouseY,
- rect
+ const pages = sigitFiles[pdfFileIndex]?.pages
+ if (pages) {
+ pages[pdfPageIndex]?.drawnFields?.splice(drawnFileIndex, 1)
}
}
+ /**
+ * Used to stop pointer click propagating to the parent elements
+ * so select can work properly
+ * @param event Pointer event
+ */
+ const handleUserSelectPointerDown = (event: React.PointerEvent) => {
+ event.stopPropagation()
+ }
+
+ /**
+ * Gets the pointer coordinates relative to a element in the `event` param
+ * @param event PointerEvent
+ * @param customTarget coordinates relative to this element, if not provided
+ * event.target will be used
+ */
+ const getPointerCoordinates = (
+ event: React.PointerEvent,
+ customTarget?: HTMLElement | null
+ ) => {
+ const target = customTarget ? customTarget : event.currentTarget
+ const rect = target.getBoundingClientRect()
+
+ // Clamp X Y within the target
+ const x = Math.min(event.clientX, rect.right) - rect.left //x position within the element.
+ const y = Math.min(event.clientY, rect.bottom) - rect.top //y position within the element.
+
+ return {
+ x,
+ y,
+ rect
+ }
+ }
/**
* 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 (
{
- onMouseMove(event, page)
+ onPointerMove={(event) => {
+ handlePointerMove(event, page)
}}
- onMouseDown={(event) => {
- onMouseDown(event, page)
+ onPointerDown={(event) => {
+ handlePointerDown(event, page)
}}
draggable="false"
src={page.image}
+ alt={`page ${pageIndex + 1} of ${file.name}`}
/>
{page.drawnFields.map((drawnField, drawnFieldIndex: number) => {
return (
{
- onDrawnFieldMouseMove(event, drawnField)
+ onPointerDown={(event) =>
+ handleDrawnFieldPointerDown(event, drawnFieldIndex)
+ }
+ onPointerMove={(event) => {
+ handleDrawnFieldPointerMove(event, drawnField, page.width)
}}
className={styles.drawingRectangle}
style={{
- left: `${drawnField.left}px`,
- top: `${drawnField.top}px`,
- width: `${drawnField.width}px`,
- height: `${drawnField.height}px`,
- pointerEvents: mouseState.clicked ? 'none' : 'all'
+ backgroundColor: drawnField.counterpart
+ ? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}4b`
+ : undefined,
+ borderColor: drawnField.counterpart
+ ? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}`
+ : undefined,
+ left: inPx(from(page.width, drawnField.left)),
+ top: inPx(from(page.width, drawnField.top)),
+ width: inPx(from(page.width, drawnField.width)),
+ height: inPx(from(page.width, drawnField.height)),
+ pointerEvents: mouseState.clicked ? 'none' : 'all',
+ touchAction: 'none',
+ opacity:
+ mouseState.dragging &&
+ activeDrawField === drawnFieldIndex
+ ? 0.8
+ : undefined
}}
>
{
- onResizeHandleMouseMove(event, drawnField)
+ onPointerDown={(event) =>
+ handleResizePointerDown(event, drawnFieldIndex)
+ }
+ onPointerMove={(event) => {
+ handleResizePointerMove(event, drawnField, page.width)
}}
className={styles.resizeHandle}
+ style={{
+ background:
+ mouseState.resizing &&
+ activeDrawField === drawnFieldIndex
+ ? 'var(--primary-main)'
+ : undefined
+ }}
>
{
- onRemoveHandleMouseDown(
+ onPointerDown={(event) => {
+ handleRemovePointerDown(
event,
- pdfFileIndex,
- pdfPageIndex,
+ fileIndex,
+ pageIndex,
drawnFieldIndex
)
}}
@@ -403,7 +436,7 @@ export const DrawPDFFields = (props: Props) => {
@@ -416,6 +449,10 @@ export const DrawPDFFields = (props: Props) => {
}}
labelId="counterparts"
label="Counterparts"
+ sx={{
+ background: 'white'
+ }}
+ renderValue={(value) => renderCounterpartValue(value)}
>
{users
.filter((u) => u.role === UserRole.signer)
@@ -444,7 +481,22 @@ export const DrawPDFFields = (props: Props) => {
key={index}
value={hexToNpub(user.pubkey)}
>
- {displayValue}
+
+ img': {
+ width: '30px',
+ height: '30px'
+ }
+ }}
+ />
+
+ {displayValue}
)
})}
@@ -461,48 +513,72 @@ export const DrawPDFFields = (props: Props) => {
)
}
- if (parsingPdf) {
- return (
-
-
-
- )
+ const renderCounterpartValue = (value: string) => {
+ const user = users.find((u) => u.pubkey === npubToHex(value))
+ if (user) {
+ let displayValue = truncate(value, {
+ length: 16
+ })
+
+ const metadata = props.metadata[user.pubkey]
+
+ if (metadata) {
+ displayValue = truncate(
+ metadata.name || metadata.display_name || metadata.username || value,
+ {
+ length: 16
+ }
+ )
+ }
+ return (
+ <>
+ img': {
+ width: '21px',
+ height: '21px'
+ }
+ }}
+ />
+ {displayValue}
+ >
+ )
+ }
+
+ return value
}
- if (!pdfFiles.length) {
+ if (parsingPdf) {
+ return
+ }
+
+ if (!sigitFiles.length) {
return ''
}
return (
-
- {selectedFiles.map((file, i) => {
- const name = file.name
- const extension = extractFileExtension(name)
- const pdfFile = pdfFiles.find((pdf) => pdf.file.name === name)
+
+ {sigitFiles.map((file, i) => {
return (
-
-
- {pdfFile ? (
- getPdfPages(pdfFile, i)
- ) : (
-
- This is a {extension} file
-
+
+
+ {file.isPdf && getPdfPages(file, i)}
+ {file.isImage && (
+
+ )}
+ {!(file.isPdf || file.isImage) && (
+
)}
- {i < selectedFiles.length - 1 && (
-
- File Separator
-
- )}
+ {i < selectedFiles.length - 1 && }
)
})}
diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss
index 142f88a..62fa688 100644
--- a/src/components/DrawPDFFields/style.module.scss
+++ b/src/components/DrawPDFFields/style.module.scss
@@ -8,17 +8,6 @@
}
.pdfImageWrapper {
- position: relative;
- -webkit-user-select: none;
- user-select: none;
-
- > img {
- display: block;
- max-width: 100%;
- max-height: 100%;
- object-fit: contain; /* Ensure the image fits within the container */
- }
-
&.drawing {
cursor: crosshair;
}
@@ -84,35 +73,8 @@
justify-content: center;
align-items: center;
bottom: -60px;
- min-width: 170px;
+ min-width: 193px;
min-height: 30px;
- background: #fff;
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;
-}
diff --git a/src/components/ExtensionFileBox.tsx b/src/components/ExtensionFileBox.tsx
new file mode 100644
index 0000000..f36d38c
--- /dev/null
+++ b/src/components/ExtensionFileBox.tsx
@@ -0,0 +1,6 @@
+interface ExtensionFileBoxProps {
+ extension: string
+}
+export const ExtensionFileBox = ({ extension }: ExtensionFileBoxProps) => (
+
This is a {extension} file
+)
diff --git a/src/components/FileDivider.tsx b/src/components/FileDivider.tsx
new file mode 100644
index 0000000..b66b8f4
--- /dev/null
+++ b/src/components/FileDivider.tsx
@@ -0,0 +1,12 @@
+import Divider from '@mui/material/Divider/Divider'
+
+export const FileDivider = () => (
+
+ File Separator
+
+)
diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx
index 53557a5..a47ace7 100644
--- a/src/components/FileList/index.tsx
+++ b/src/components/FileList/index.tsx
@@ -22,26 +22,26 @@ const FileList = ({
const isActive = (file: CurrentUserFile) => file.id === currentFile.id
return (
-
-
- {files.map((file: CurrentUserFile) => (
- - setCurrentFile(file)}
- >
-
{file.id}
-
+
-
+
+ {currentUserFile.isHashValid && (
+
+ )}
+
+
+ ))}
+
diff --git a/src/components/FileList/style.module.scss b/src/components/FileList/style.module.scss
index 22d8515..a05fcbd 100644
--- a/src/components/FileList/style.module.scss
+++ b/src/components/FileList/style.module.scss
@@ -1,12 +1,3 @@
-.container {
- border-radius: 4px;
- background: white;
- padding: 15px;
- display: flex;
- flex-direction: column;
- grid-gap: 0px;
-}
-
.filesPageContainer {
width: 100%;
display: grid;
@@ -15,18 +6,6 @@
flex-grow: 1;
}
-ul {
- list-style-type: none; /* Removes bullet points */
- margin: 0; /* Removes default margin */
- padding: 0; /* Removes default padding */
-}
-
-li {
- list-style-type: none; /* Removes the bullets */
- margin: 0; /* Removes any default margin */
- padding: 0; /* Removes any default padding */
-}
-
.wrap {
display: flex;
flex-direction: column;
@@ -34,14 +13,16 @@ li {
}
.files {
+ border-radius: 4px;
+ background: white;
+ padding: 15px;
+
display: flex;
flex-direction: column;
width: 100%;
grid-gap: 15px;
- max-height: 350px;
- overflow: auto;
- padding: 0 5px 0 0;
- margin: 0 -5px 0 0;
+ overflow-y: auto;
+ overflow-x: none;
}
.files::-webkit-scrollbar {
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
index eac4166..17140e4 100644
--- a/src/components/Footer/Footer.tsx
+++ b/src/components/Footer/Footer.tsx
@@ -4,125 +4,128 @@ import styles from './style.module.scss'
import { Container } from '../Container'
import nostrImage from '../../assets/images/nostr.gif'
import { appPublicRoutes } from '../../routes'
+import { createPortal } from 'react-dom'
-export const Footer = () => (
-
-)
+
+
+ ,
+ document.getElementById('root')!
+ )
diff --git a/src/components/LoadingSpinner/index.tsx b/src/components/LoadingSpinner/index.tsx
index 980a763..2c6f4e5 100644
--- a/src/components/LoadingSpinner/index.tsx
+++ b/src/components/LoadingSpinner/index.tsx
@@ -1,18 +1,35 @@
import styles from './style.module.scss'
interface Props {
- desc: string
+ desc?: string
+ variant?: 'small' | 'default'
}
export const LoadingSpinner = (props: Props) => {
- const { desc } = props
+ const { desc, variant = 'default' } = props
- return (
-
- )
+ switch (variant) {
+ case 'small':
+ return (
+
+ )
+
+ default:
+ return (
+
+ )
+ }
}
diff --git a/src/components/LoadingSpinner/style.module.scss b/src/components/LoadingSpinner/style.module.scss
index 75b2609..e1a5978 100644
--- a/src/components/LoadingSpinner/style.module.scss
+++ b/src/components/LoadingSpinner/style.module.scss
@@ -2,34 +2,48 @@
.loadingSpinnerOverlay {
position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
+ inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
+ backdrop-filter: blur(10px);
+}
- .loadingSpinnerContainer {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
+.loadingSpinnerContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ &[data-variant='default'] {
+ width: 100%;
+ max-width: 500px;
+ margin: 25px 20px;
+ background: $overlay-background-color;
+ border-radius: 4px;
+ box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
}
-
- .loadingSpinner {
- background: url('/favicon.png') no-repeat center / cover;
- width: 40px;
- height: 40px;
- animation: spin 1s linear infinite;
+ &[data-variant='small'] {
+ min-height: 250px;
}
}
+.loadingSpinner {
+ background: url('/favicon.png') no-repeat center / cover;
+ margin: 40px 25px;
+ width: 65px;
+ height: 65px;
+ animation: spin 1s linear infinite;
+}
+
.loadingSpinnerDesc {
- color: white;
- margin-top: 13px;
+ width: 100%;
+ padding: 15px;
+ border-top: solid 1px rgba(0, 0, 0, 0.1);
+ text-align: center;
+ color: rgba(0, 0, 0, 0.5);
font-size: 16px;
font-weight: 400;
diff --git a/src/components/MarkFormField/style.module.scss b/src/components/MarkFormField/style.module.scss
index 1275038..9f4b092 100644
--- a/src/components/MarkFormField/style.module.scss
+++ b/src/components/MarkFormField/style.module.scss
@@ -1,11 +1,19 @@
+@import '../../styles/sizes.scss';
+
.container {
- width: 100%;
display: flex;
flex-direction: column;
position: fixed;
- bottom: 0;
- right: 0;
- left: 0;
+
+ @media only screen and (min-width: 768px) {
+ bottom: 0;
+ right: 0;
+ left: 0;
+ }
+ bottom: $tabs-height + 5px;
+ right: 5px;
+ left: 5px;
+
align-items: center;
z-index: 1000;
@@ -107,7 +115,7 @@
.actions {
background: white;
width: 100%;
- border-radius: 4px;
+ border-radius: 5px;
padding: 10px 20px;
display: none;
flex-direction: column;
diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx
index c502bb4..f1dbe87 100644
--- a/src/components/PDFView/PdfItem.tsx
+++ b/src/components/PDFView/PdfItem.tsx
@@ -1,12 +1,13 @@
-import { PdfFile } from '../../types/drawing.ts'
import { CurrentUserMark, Mark } from '../../types/mark.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: SigitFile
selectedMark: CurrentUserMark | null
selectedMarkValue: string
}
@@ -15,7 +16,7 @@ interface PdfItemProps {
* Responsible for displaying pages of a single Pdf File.
*/
const PdfItem = ({
- pdfFile,
+ file,
currentUserMarks,
handleMarkClick,
selectedMarkValue,
@@ -31,19 +32,27 @@ const PdfItem = ({
const filterMarksByPage = (marks: Mark[], page: number): Mark[] => {
return marks.filter((mark) => mark.location.page === page)
}
- return pdfFile.pages.map((page, i) => {
- return (
-
- )
- })
+ if (file.isPdf) {
+ return file.pages?.map((page, i) => {
+ return (
+
+ )
+ })
+ } else if (file.isImage) {
+ return
+ } else {
+ return
+ }
}
export default PdfItem
diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx
index d93c2b2..db57800 100644
--- a/src/components/PDFView/PdfMarkItem.tsx
+++ b/src/components/PDFView/PdfMarkItem.tsx
@@ -1,42 +1,56 @@
import { CurrentUserMark } from '../../types/mark.ts'
import styles from '../DrawPDFFields/style.module.scss'
-import { inPx } from '../../utils/pdf.ts'
+import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
+import { useScale } from '../../hooks/useScale.tsx'
+import { forwardRef } from 'react'
+import { npubToHex } from '../../utils/nostr.ts'
interface PdfMarkItemProps {
userMark: CurrentUserMark
handleMarkClick: (id: number) => void
selectedMarkValue: string
selectedMark: CurrentUserMark | null
+ pageWidth: number
}
/**
* Responsible for display an individual Pdf Mark.
*/
-const PdfMarkItem = ({
- selectedMark,
- handleMarkClick,
- selectedMarkValue,
- userMark
-}: PdfMarkItemProps) => {
- const { location } = userMark.mark
- const handleClick = () => handleMarkClick(userMark.mark.id)
- const isEdited = () => selectedMark?.mark.id === userMark.mark.id
- const getMarkValue = () =>
- isEdited() ? selectedMarkValue : userMark.currentValue
- return (
-
- {getMarkValue()}
-
- )
-}
+const PdfMarkItem = forwardRef
(
+ (
+ { selectedMark, handleMarkClick, selectedMarkValue, userMark, pageWidth },
+ ref
+ ) => {
+ const { location } = userMark.mark
+ const handleClick = () => handleMarkClick(userMark.mark.id)
+ const isEdited = () => selectedMark?.mark.id === userMark.mark.id
+ const getMarkValue = () =>
+ isEdited() ? selectedMarkValue : userMark.currentValue
+ const { from } = useScale()
+ return (
+
+ {getMarkValue()}
+
+ )
+ }
+)
export default PdfMarkItem
diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx
index 9fff924..61968b0 100644
--- a/src/components/PDFView/PdfMarking.tsx
+++ b/src/components/PDFView/PdfMarking.tsx
@@ -10,12 +10,16 @@ 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'
import { UsersDetails } from '../UsersDetails.tsx'
import { Meta } from '../../types'
+import {
+ faCircleInfo,
+ faFileDownload,
+ faPen
+} from '@fortawesome/free-solid-svg-icons'
interface PdfMarkingProps {
currentUserMarks: CurrentUserMark[]
@@ -24,7 +28,7 @@ interface PdfMarkingProps {
meta: Meta | null
otherUserMarks: Mark[]
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
- setIsReadyToSign: (isReadyToSign: boolean) => void
+ setIsMarksCompleted: (isMarksCompleted: boolean) => void
setUpdatedMarks: (markToUpdate: Mark) => void
}
@@ -38,7 +42,7 @@ const PdfMarking = (props: PdfMarkingProps) => {
const {
files,
currentUserMarks,
- setIsReadyToSign,
+ setIsMarksCompleted,
setCurrentUserMarks,
setUpdatedMarks,
handleDownload,
@@ -102,7 +106,7 @@ const PdfMarking = (props: PdfMarkingProps) => {
)
setCurrentUserMarks(updatedCurrentUserMarks)
setSelectedMark(null)
- setIsReadyToSign(true)
+ setIsMarksCompleted(true)
setUpdatedMarks(updatedMark.mark)
}
@@ -133,22 +137,21 @@ const PdfMarking = (props: PdfMarkingProps) => {
}
right={meta !== null &&
}
+ leftIcon={faFileDownload}
+ centerIcon={faPen}
+ rightIcon={faCircleInfo}
>
-
- {currentUserMarks?.length > 0 && (
-
- )}
-
+ {currentUserMarks?.length > 0 && (
+
+ )}
{selectedMark !== null && (
void
otherUserMarks: Mark[]
@@ -18,6 +21,8 @@ interface PdfPageProps {
* Responsible for rendering a single Pdf Page and its Marks
*/
const PdfPageItem = ({
+ fileName,
+ pageIndex,
page,
currentUserMarks,
handleMarkClick,
@@ -28,45 +33,49 @@ 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)[]>([])
+ const { from } = useScale()
+
return (
-
-
+
+
{currentUserMarks.map((m, i) => (
-
(markRefs.current[m.id] = el)}>
-
-
- ))}
- {otherUserMarks.map((m, i) => (
-
- {m.value}
-
+ ref={(el) => (markRefs.current[m.id] = el)}
+ handleMarkClick={handleMarkClick}
+ selectedMarkValue={selectedMarkValue}
+ userMark={m}
+ selectedMark={selectedMark}
+ pageWidth={page.width}
+ />
))}
+ {otherUserMarks.map((m, i) => {
+ return (
+
+ {m.value}
+
+ )
+ })}
)
}
diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx
index ef765f0..acd1874 100644
--- a/src/components/PDFView/index.tsx
+++ b/src/components/PDFView/index.tsx
@@ -1,8 +1,10 @@
-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'
+import { LoadingSpinner } from '../LoadingSpinner/index.tsx'
interface PdfViewProps {
currentFile: CurrentUserFile | null
@@ -29,10 +31,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,29 +48,36 @@ const PdfView = ({
const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean =>
index !== files.length - 1
return (
- <>
- {files.map((currentUserFile, index, arr) => {
- const { hash, pdfFile, id } = currentUserFile
- if (!hash) return
- return (
-
(pdfRefs.current[id] = el)}
- key={index}
- >
-
- {isNotLastPdfFile(index, arr) &&
File Separator}
-
- )
- })}
- >
+
+ {files.length > 0 ? (
+ files.map((currentUserFile, index, arr) => {
+ const { hash, file, id } = currentUserFile
+
+ if (!hash) return
+ return (
+
+ (pdfRefs.current[id] = el)}
+ >
+
+
+ {isNotLastPdfFile(index, arr) && }
+
+ )
+ })
+ ) : (
+
+ )}
+
)
}
diff --git a/src/components/PDFView/style.module.scss b/src/components/PDFView/style.module.scss
index 3a893d4..870057a 100644
--- a/src/components/PDFView/style.module.scss
+++ b/src/components/PDFView/style.module.scss
@@ -1,33 +1,7 @@
-.imageWrapper {
- display: flex;
- justify-content: center;
- align-items: center;
- width: 100%; /* Adjust as needed */
- height: 100%; /* Adjust as needed */
- overflow: hidden; /* Ensure no overflow */
- border: 1px solid #ccc; /* Optional: for visual debugging */
- background-color: #e0f7fa; /* Optional: for visual debugging */
-}
-
-.image {
- max-width: 100%;
- max-height: 100%;
- object-fit: contain; /* Ensure the image fits within the container */
-}
-
.container {
display: flex;
width: 100%;
flex-direction: column;
-
-}
-
-.pdfView {
- display: flex;
- flex-direction: column;
- width: 100%;
- height: 100%;
- gap: 10px;
}
.otherUserMarksDisplay {
diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx
index 3681cfd..bddae82 100644
--- a/src/components/UsersDetails.tsx/index.tsx
+++ b/src/components/UsersDetails.tsx/index.tsx
@@ -1,7 +1,6 @@
import { Divider, Tooltip } from '@mui/material'
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
import {
- extractFileExtensions,
formatTimestamp,
fromUnixTimestamp,
hexToNpub,
@@ -28,6 +27,7 @@ import { State } from '../../store/rootReducer'
import { TooltipChild } from '../TooltipChild'
import { DisplaySigner } from '../DisplaySigner'
import { Meta } from '../../types'
+import { extractFileExtensions } from '../../utils/file'
interface UsersDetailsProps {
meta: Meta
@@ -118,32 +118,44 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
)
})}
- {viewers.map((signer) => {
- const pubkey = npubToHex(signer)!
- const profile = profiles[pubkey]
-
- return (
-
-
-
-
-
- )
- })}
+
+ {viewers.length > 0 && (
+ <>
+ Viewers
+
+
+ {viewers.map((signer) => {
+ const pubkey = npubToHex(signer)!
+ const profile = profiles[pubkey]
+
+ return (
+
+
+
+
+
+ )
+ })}
+
+
+ >
+ )}
Details
diff --git a/src/controllers/RelayController.ts b/src/controllers/RelayController.ts
index 40945ad..df33b4b 100644
--- a/src/controllers/RelayController.ts
+++ b/src/controllers/RelayController.ts
@@ -1,5 +1,9 @@
import { Event, Filter, Relay } from 'nostr-tools'
-import { normalizeWebSocketURL, timeout } from '../utils'
+import {
+ settleAllFullfilfedPromises,
+ normalizeWebSocketURL,
+ timeout
+} from '../utils'
import { SIGIT_RELAY } from '../utils/const'
/**
@@ -105,24 +109,11 @@ export class RelayController {
}
// connect to all specified relays
- const relayPromises = relayUrls.map((relayUrl) =>
- this.connectRelay(relayUrl)
+ const relays = await settleAllFullfilfedPromises(
+ relayUrls,
+ this.connectRelay
)
- // Use Promise.allSettled to wait for all promises to settle
- const results = await Promise.allSettled(relayPromises)
-
- // Extract non-null values from fulfilled promises in a single pass
- const relays = results.reduce
((acc, result) => {
- if (result.status === 'fulfilled') {
- const value = result.value
- if (value) {
- acc.push(value)
- }
- }
- return acc
- }, [])
-
// Check if any relays are connected
if (relays.length === 0) {
throw new Error('No relay is connected to fetch events!')
@@ -228,23 +219,10 @@ export class RelayController {
}
// connect to all specified relays
- const relayPromises = relayUrls.map((relayUrl) => {
- return this.connectRelay(relayUrl)
- })
-
- // Use Promise.allSettled to wait for all promises to settle
- const results = await Promise.allSettled(relayPromises)
-
- // Extract non-null values from fulfilled promises in a single pass
- const relays = results.reduce((acc, result) => {
- if (result.status === 'fulfilled') {
- const value = result.value
- if (value) {
- acc.push(value)
- }
- }
- return acc
- }, [])
+ const relays = await settleAllFullfilfedPromises(
+ relayUrls,
+ this.connectRelay
+ )
// Check if any relays are connected
if (relays.length === 0) {
@@ -292,24 +270,11 @@ export class RelayController {
}
// connect to all specified relays
- const relayPromises = relayUrls.map((relayUrl) =>
- this.connectRelay(relayUrl)
+ const relays = await settleAllFullfilfedPromises(
+ relayUrls,
+ this.connectRelay
)
- // Use Promise.allSettled to wait for all promises to settle
- const results = await Promise.allSettled(relayPromises)
-
- // Extract non-null values from fulfilled promises in a single pass
- const relays = results.reduce((acc, result) => {
- if (result.status === 'fulfilled') {
- const value = result.value
- if (value) {
- acc.push(value)
- }
- }
- return acc
- }, [])
-
// Check if any relays are connected
if (relays.length === 0) {
throw new Error('No relay is connected to publish event!')
diff --git a/src/hooks/useScale.tsx b/src/hooks/useScale.tsx
new file mode 100644
index 0000000..406928b
--- /dev/null
+++ b/src/hooks/useScale.tsx
@@ -0,0 +1,46 @@
+import { useEffect, useState } from 'react'
+import { singletonHook } from 'react-singleton-hook'
+import { getInnerContentWidth } from '../utils/pdf'
+
+const noScaleInit = {
+ to: (_: number, v: number) => v,
+ from: (_: number, v: number) => v
+}
+
+const useScaleImpl = () => {
+ const [width, setWidth] = useState(getInnerContentWidth())
+
+ // Get the scale based on the original width
+ const scale = (originalWidth: number) => {
+ return width / originalWidth
+ }
+
+ // Get the original pixel value
+ const to = (originalWidth: number, value: number) => {
+ return value / scale(originalWidth)
+ }
+
+ // Get the scaled pixel value
+ const from = (originalWidth: number, value: number) => {
+ return value * scale(originalWidth)
+ }
+
+ const resize = () => {
+ setWidth(getInnerContentWidth())
+ }
+
+ useEffect(() => {
+ resize()
+
+ window.addEventListener('resize', resize)
+ return () => {
+ window.removeEventListener('resize', resize)
+ }
+ }, [])
+
+ return { to, from }
+}
+
+export const useScale = singletonHook(noScaleInit, useScaleImpl, {
+ unmountIfNoConsumers: true
+})
diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx
index fea5154..088940e 100644
--- a/src/hooks/useSigitMeta.tsx
+++ b/src/hooks/useSigitMeta.tsx
@@ -11,7 +11,6 @@ import {
hexToNpub,
parseNostrEvent,
parseCreateSignatureEventContent,
- SigitMetaParseError,
SigitStatus,
SignStatus
} from '../utils'
@@ -21,6 +20,7 @@ import { Event } from 'nostr-tools'
import store from '../store/store'
import { AuthState } from '../store/auth/types'
import { NostrController } from '../controllers'
+import { MetaParseError } from '../types/errors/MetaParseError'
/**
* Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,
@@ -247,7 +247,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
)
}
} catch (error) {
- if (error instanceof SigitMetaParseError) {
+ if (error instanceof MetaParseError) {
toast.error(error.message)
}
console.error(error)
diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx
index ac233cc..f3962eb 100644
--- a/src/layouts/Main.tsx
+++ b/src/layouts/Main.tsx
@@ -26,7 +26,6 @@ import {
} from '../utils'
import { useAppSelector } from '../hooks'
import styles from './style.module.scss'
-import { Footer } from '../components/Footer/Footer'
export const MainLayout = () => {
const dispatch: Dispatch = useDispatch()
@@ -160,7 +159,6 @@ export const MainLayout = () => {
>
-
>
)
}
diff --git a/src/layouts/StickySideColumns.module.scss b/src/layouts/StickySideColumns.module.scss
index 7495cad..a116720 100644
--- a/src/layouts/StickySideColumns.module.scss
+++ b/src/layouts/StickySideColumns.module.scss
@@ -3,9 +3,33 @@
.container {
display: grid;
- grid-template-columns: 0.75fr 1.5fr 0.75fr;
- grid-gap: 30px;
- flex-grow: 1;
+
+ @media only screen and (max-width: 767px) {
+ gap: 20px;
+ grid-auto-flow: column;
+ grid-auto-columns: 100%;
+
+ // Hide Scrollbar and let's use tabs to navigate
+ -ms-overflow-style: none; /* Internet Explorer 10+ */
+ scrollbar-width: none; /* Firefox */
+ &::-webkit-scrollbar {
+ display: none; /* Safari and Chrome */
+ }
+ overflow-x: auto;
+ overscroll-behavior-inline: contain;
+ scroll-snap-type: inline mandatory;
+
+ > * {
+ scroll-margin-top: $header-height + $body-vertical-padding;
+ scroll-snap-align: start;
+ scroll-snap-stop: always; // Touch devices will always stop on each element
+ }
+ }
+
+ @media only screen and (min-width: 768px) {
+ grid-template-columns: 0.75fr 1.5fr 0.75fr;
+ gap: 30px;
+ }
}
.sidesWrap {
@@ -16,21 +40,58 @@
}
.sides {
- position: sticky;
- top: $header-height + $body-vertical-padding;
+ @media only screen and (min-width: 768px) {
+ position: sticky;
+ top: $header-height + $body-vertical-padding;
+ }
+ > :first-child {
+ max-height: calc(
+ 100dvh - $header-height - $body-vertical-padding * 2 - $tabs-height
+ );
+ }
}
-.files {
- display: flex;
- flex-direction: column;
- grid-gap: 15px;
+.scrollAdjust {
+ @media only screen and (max-width: 767px) {
+ max-height: calc(
+ 100svh - $header-height - $body-vertical-padding * 2 - $tabs-height
+ );
+ overflow-y: auto;
+ }
}
+
.content {
- padding: 10px;
- border: 10px solid $overlay-background-color;
- border-radius: 4px;
-
- max-width: 590px;
- width: 590px;
- margin: 0 auto;
+ @media only screen and (min-width: 768px) {
+ padding: 10px;
+ border: 10px solid $overlay-background-color;
+ border-radius: 4px;
+ }
+}
+
+.navTabs {
+ display: none;
+ position: fixed;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ height: $tabs-height;
+ z-index: 2;
+ background: $overlay-background-color;
+ box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
+
+ padding: 5px;
+ gap: 5px;
+
+ @media only screen and (max-width: 767px) {
+ display: flex;
+ }
+
+ > li {
+ flex-grow: 1;
+ }
+}
+
+.active {
+ background-color: $primary-main !important;
+ color: white !important;
}
diff --git a/src/layouts/StickySideColumns.tsx b/src/layouts/StickySideColumns.tsx
index 1ada87f..c460fbd 100644
--- a/src/layouts/StickySideColumns.tsx
+++ b/src/layouts/StickySideColumns.tsx
@@ -1,26 +1,147 @@
-import { PropsWithChildren, ReactNode } from 'react'
+import {
+ PropsWithChildren,
+ ReactNode,
+ useEffect,
+ useRef,
+ useState
+} from 'react'
import styles from './StickySideColumns.module.scss'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
+import { Button } from '@mui/material'
interface StickySideColumnsProps {
- left?: ReactNode
- right?: ReactNode
+ left: ReactNode
+ right: ReactNode
+ leftIcon: IconDefinition
+ centerIcon: IconDefinition
+ rightIcon: IconDefinition
}
+const DEFAULT_TAB = 'nav-content'
export const StickySideColumns = ({
left,
right,
+ leftIcon,
+ centerIcon,
+ rightIcon,
children
}: PropsWithChildren) => {
+ const [tab, setTab] = useState(DEFAULT_TAB)
+ const ref = useRef(null)
+ const tabsRefs = useRef<{ [id: string]: HTMLDivElement | null }>({})
+ const handleNavClick = (id: string) => {
+ if (ref.current && tabsRefs.current) {
+ const x = tabsRefs.current[id]?.offsetLeft
+ ref.current.scrollTo({
+ left: x,
+ behavior: 'smooth'
+ })
+ }
+ }
+ const isActive = (id: string) => id === tab
+
+ useEffect(() => {
+ setTab(DEFAULT_TAB)
+ handleNavClick(DEFAULT_TAB)
+ }, [])
+
+ useEffect(() => {
+ const tabs = tabsRefs.current
+ // Set up the observer
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ setTab(entry.target.id)
+ }
+ })
+ },
+ {
+ root: ref.current,
+ threshold: 0.5,
+ rootMargin: '-20px'
+ }
+ )
+
+ if (tabs) {
+ Object.values(tabs).forEach((tab) => {
+ if (tab) observer.observe(tab)
+ })
+ }
+
+ return () => {
+ if (tabs) {
+ Object.values(tabs).forEach((tab) => {
+ if (tab) observer.unobserve(tab)
+ })
+ }
+ }
+ }, [])
+
return (
-
-
-
{left}
+ <>
+
+
(tabsRefs.current['nav-left'] = tab)}
+ >
+
{left}
+
+
(tabsRefs.current['nav-content'] = tab)}
+ >
+
+ {children}
+
+
+
(tabsRefs.current['nav-right'] = tab)}
+ >
+
{right}
+
-
{children}
-
-
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+ >
)
}
diff --git a/src/layouts/style.module.scss b/src/layouts/style.module.scss
index c1aee30..6c8aa59 100644
--- a/src/layouts/style.module.scss
+++ b/src/layouts/style.module.scss
@@ -4,5 +4,4 @@
.main {
flex-grow: 1;
padding: $header-height + $body-vertical-padding 0 $body-vertical-padding 0;
- background-color: $body-background-color;
}
diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx
index 8a73012..f0bd6b9 100644
--- a/src/pages/create/index.tsx
+++ b/src/pages/create/index.tsx
@@ -1,20 +1,13 @@
-import {
- Button,
- FormHelperText,
- ListItemIcon,
- ListItemText,
- MenuItem,
- Select,
- TextField,
- Tooltip
-} from '@mui/material'
+import styles from './style.module.scss'
+import { Button, FormHelperText, TextField, Tooltip } from '@mui/material'
import type { Identifier, XYCoord } from 'dnd-core'
import saveAs from 'file-saver'
import JSZip from 'jszip'
import { Event, kinds } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react'
import { DndProvider, useDrag, useDrop } from 'react-dnd'
-import { HTML5Backend } from 'react-dnd-html5-backend'
+import { MultiBackend } from 'react-dnd-multi-backend'
+import { HTML5toTouch } from 'rdndmb-html5-to-touch'
import { useSelector } from 'react-redux'
import { useLocation, useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
@@ -49,9 +42,8 @@ import {
uploadToFileStorage
} from '../../utils'
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'
@@ -66,6 +58,8 @@ import {
faCreditCard,
faEllipsis,
faEye,
+ faFile,
+ faFileCirclePlus,
faGripLines,
faHeading,
faIdCard,
@@ -80,9 +74,11 @@ import {
faStamp,
faT,
faTableCellsLarge,
+ faToolbox,
faTrash,
faUpload
} from '@fortawesome/free-solid-svg-icons'
+import { SigitFile } from '../../utils/file.ts'
export const CreatePage = () => {
const navigate = useNavigate()
@@ -125,115 +121,115 @@ export const CreatePage = () => {
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
{}
)
- const [drawnPdfs, setDrawnPdfs] = useState
([])
+ const [drawnFiles, setDrawnFiles] = useState([])
const [selectedTool, setSelectedTool] = useState()
const [toolbox] = useState([
{
identifier: MarkType.TEXT,
- icon: ,
+ icon: faT,
label: 'Text',
active: true
},
{
identifier: MarkType.SIGNATURE,
- icon: ,
+ icon: faSignature,
label: 'Signature',
active: false
},
{
identifier: MarkType.JOBTITLE,
- icon: ,
+ icon: faBriefcase,
label: 'Job Title',
active: false
},
{
identifier: MarkType.FULLNAME,
- icon: ,
+ icon: faIdCard,
label: 'Full Name',
active: false
},
{
identifier: MarkType.INITIALS,
- icon: ,
+ icon: faHeading,
label: 'Initials',
active: false
},
{
identifier: MarkType.DATETIME,
- icon: ,
+ icon: faClock,
label: 'Date Time',
active: false
},
{
identifier: MarkType.DATE,
- icon: ,
+ icon: faCalendarDays,
label: 'Date',
active: false
},
{
identifier: MarkType.NUMBER,
- icon: ,
+ icon: fa1,
label: 'Number',
active: false
},
{
identifier: MarkType.IMAGES,
- icon: ,
+ icon: faImage,
label: 'Images',
active: false
},
{
identifier: MarkType.CHECKBOX,
- icon: ,
+ icon: faSquareCheck,
label: 'Checkbox',
active: false
},
{
identifier: MarkType.MULTIPLE,
- icon: ,
+ icon: faCheckDouble,
label: 'Multiple',
active: false
},
{
identifier: MarkType.FILE,
- icon: ,
+ icon: faPaperclip,
label: 'File',
active: false
},
{
identifier: MarkType.RADIO,
- icon: ,
+ icon: faCircleDot,
label: 'Radio',
active: false
},
{
identifier: MarkType.SELECT,
- icon: ,
+ icon: faSquareCaretDown,
label: 'Select',
active: false
},
{
identifier: MarkType.CELLS,
- icon: ,
+ icon: faTableCellsLarge,
label: 'Cells',
active: false
},
{
identifier: MarkType.STAMP,
- icon: ,
+ icon: faStamp,
label: 'Stamp',
active: false
},
{
identifier: MarkType.PAYMENT,
- icon: ,
+ icon: faCreditCard,
label: 'Payment',
active: false
},
{
identifier: MarkType.PHONE,
- icon: ,
+ icon: faPhone,
label: 'Phone',
active: false
}
@@ -456,10 +452,8 @@ export const CreatePage = () => {
return false
}
- if (users.length === 0) {
- toast.error(
- 'No signer/viewer is provided. At least add one signer or viewer.'
- )
+ if (!users.some((u) => u.role === UserRole.signer)) {
+ toast.error('No signer is provided. At least add one signer.')
return false
}
@@ -507,26 +501,31 @@ 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) => {
+ if (!drawnField.counterpart) {
+ throw new Error('Missing counterpart')
+ }
+ 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 }
@@ -667,6 +666,7 @@ export const CreatePage = () => {
}
const generateCreateSignature = async (
+ markConfig: Mark[],
fileHashes: {
[key: string]: string
},
@@ -674,7 +674,6 @@ export const CreatePage = () => {
) => {
const signers = users.filter((user) => user.role === UserRole.signer)
const viewers = users.filter((user) => user.role === UserRole.viewer)
- const markConfig = createMarks(fileHashes)
const content: CreateSignatureEventContent = {
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
@@ -718,136 +717,133 @@ export const CreatePage = () => {
}
const handleCreate = async () => {
- if (!validateInputs()) return
+ try {
+ if (!validateInputs()) return
- setIsLoading(true)
- setLoadingSpinnerDesc('Generating file hashes')
- const fileHashes = await generateFileHashes()
- if (!fileHashes) {
+ setIsLoading(true)
+ setLoadingSpinnerDesc('Generating file hashes')
+ const fileHashes = await generateFileHashes()
+ if (!fileHashes) return
+
+ setLoadingSpinnerDesc('Generating encryption key')
+ const encryptionKey = await generateEncryptionKey()
+
+ if (await isOnline()) {
+ setLoadingSpinnerDesc('generating files.zip')
+ const arrayBuffer = await generateFilesZip()
+ if (!arrayBuffer) return
+
+ setLoadingSpinnerDesc('Encrypting files.zip')
+ const encryptedArrayBuffer = await encryptZipFile(
+ arrayBuffer,
+ encryptionKey
+ )
+
+ const markConfig = createMarks(fileHashes)
+
+ setLoadingSpinnerDesc('Uploading files.zip to file storage')
+ const fileUrl = await uploadFile(encryptedArrayBuffer)
+ if (!fileUrl) return
+
+ setLoadingSpinnerDesc('Generating create signature')
+ const createSignature = await generateCreateSignature(
+ markConfig,
+ fileHashes,
+ fileUrl
+ )
+ if (!createSignature) return
+
+ setLoadingSpinnerDesc('Generating keys for decryption')
+
+ // generate key pairs for decryption
+ const pubkeys = users.map((user) => user.pubkey)
+ // also add creator in the list
+ if (pubkeys.includes(usersPubkey!)) {
+ pubkeys.push(usersPubkey!)
+ }
+
+ const keys = await generateKeys(pubkeys, encryptionKey)
+ if (!keys) return
+
+ const meta: Meta = {
+ createSignature,
+ keys,
+ modifiedAt: unixNow(),
+ docSignatures: {}
+ }
+
+ setLoadingSpinnerDesc('Updating user app data')
+ const event = await updateUsersAppData(meta)
+ if (!event) return
+
+ setLoadingSpinnerDesc('Sending notifications to counterparties')
+ const promises = sendNotifications(meta)
+
+ await Promise.all(promises)
+ .then(() => {
+ toast.success('Notifications sent successfully')
+ })
+ .catch(() => {
+ toast.error('Failed to publish notifications')
+ })
+
+ navigate(appPrivateRoutes.sign, { state: { meta: meta } })
+ } else {
+ const zip = new JSZip()
+
+ selectedFiles.forEach((file) => {
+ zip.file(`files/${file.name}`, file)
+ })
+
+ const markConfig = createMarks(fileHashes)
+
+ setLoadingSpinnerDesc('Generating create signature')
+ const createSignature = await generateCreateSignature(
+ markConfig,
+ fileHashes,
+ ''
+ )
+ if (!createSignature) return
+
+ const meta: Meta = {
+ createSignature,
+ modifiedAt: unixNow(),
+ docSignatures: {}
+ }
+
+ // add meta to zip
+ try {
+ const stringifiedMeta = JSON.stringify(meta, null, 2)
+ zip.file('meta.json', stringifiedMeta)
+ } catch (err) {
+ console.error(err)
+ toast.error('An error occurred in converting meta json to string')
+ return null
+ }
+
+ const arrayBuffer = await generateZipFile(zip)
+ if (!arrayBuffer) return
+
+ setLoadingSpinnerDesc('Encrypting zip file')
+ const encryptedArrayBuffer = await encryptZipFile(
+ arrayBuffer,
+ encryptionKey
+ )
+
+ await handleOfflineFlow(encryptedArrayBuffer, encryptionKey)
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ toast.error(error.message)
+ }
+ console.error(error)
+ } finally {
setIsLoading(false)
- return
- }
-
- setLoadingSpinnerDesc('Generating encryption key')
- const encryptionKey = await generateEncryptionKey()
-
- if (await isOnline()) {
- setLoadingSpinnerDesc('generating files.zip')
- const arrayBuffer = await generateFilesZip()
- if (!arrayBuffer) {
- setIsLoading(false)
- return
- }
-
- setLoadingSpinnerDesc('Encrypting files.zip')
- const encryptedArrayBuffer = await encryptZipFile(
- arrayBuffer,
- encryptionKey
- )
-
- setLoadingSpinnerDesc('Uploading files.zip to file storage')
- const fileUrl = await uploadFile(encryptedArrayBuffer)
- if (!fileUrl) {
- setIsLoading(false)
- return
- }
-
- setLoadingSpinnerDesc('Generating create signature')
- const createSignature = await generateCreateSignature(fileHashes, fileUrl)
- if (!createSignature) {
- setIsLoading(false)
- return
- }
-
- setLoadingSpinnerDesc('Generating keys for decryption')
-
- // generate key pairs for decryption
- const pubkeys = users.map((user) => user.pubkey)
- // also add creator in the list
- if (pubkeys.includes(usersPubkey!)) {
- pubkeys.push(usersPubkey!)
- }
-
- const keys = await generateKeys(pubkeys, encryptionKey)
-
- if (!keys) {
- setIsLoading(false)
- return
- }
- const meta: Meta = {
- createSignature,
- keys,
- modifiedAt: unixNow(),
- docSignatures: {}
- }
-
- setLoadingSpinnerDesc('Updating user app data')
- const event = await updateUsersAppData(meta)
- if (!event) {
- setIsLoading(false)
- return
- }
-
- setLoadingSpinnerDesc('Sending notifications to counterparties')
- const promises = sendNotifications(meta)
-
- await Promise.all(promises)
- .then(() => {
- toast.success('Notifications sent successfully')
- })
- .catch(() => {
- toast.error('Failed to publish notifications')
- })
-
- navigate(appPrivateRoutes.sign, { state: { meta: meta } })
- } else {
- const zip = new JSZip()
-
- selectedFiles.forEach((file) => {
- zip.file(`files/${file.name}`, file)
- })
-
- setLoadingSpinnerDesc('Generating create signature')
- const createSignature = await generateCreateSignature(fileHashes, '')
- if (!createSignature) {
- setIsLoading(false)
- return
- }
-
- const meta: Meta = {
- createSignature,
- modifiedAt: unixNow(),
- docSignatures: {}
- }
-
- // add meta to zip
- try {
- const stringifiedMeta = JSON.stringify(meta, null, 2)
- zip.file('meta.json', stringifiedMeta)
- } catch (err) {
- console.error(err)
- toast.error('An error occurred in converting meta json to string')
- return null
- }
-
- const arrayBuffer = await generateZipFile(zip)
- if (!arrayBuffer) {
- setIsLoading(false)
- return
- }
-
- setLoadingSpinnerDesc('Encrypting zip file')
- const encryptedArrayBuffer = await encryptZipFile(
- arrayBuffer,
- encryptionKey
- )
-
- await handleOfflineFlow(encryptedArrayBuffer, encryptionKey)
}
}
- const onDrawFieldsChange = (pdfFiles: PdfFile[]) => {
- setDrawnPdfs(pdfFiles)
+ const onDrawFieldsChange = (sigitFiles: SigitFile[]) => {
+ setDrawnFiles(sigitFiles)
}
if (authUrl) {
@@ -870,27 +866,18 @@ export const CreatePage = () => {
}
right={
-
- setUserInput(e.target.value)}
- onKeyDown={handleInputKeyDown}
- error={!!error}
- fullWidth
- sx={{
- fontSize: '16px',
- '& .MuiInputBase-input': {
- padding: '7px 14px'
- },
- '& .MuiOutlinedInput-notchedOutline': {
- display: 'none'
- }
- }}
- />
-
-
-
-
-
+
{
moveSigner={moveSigner}
/>
-
+
+
+ setUserInput(e.target.value)}
+ onKeyDown={handleInputKeyDown}
+ error={!!error}
+ />
+
+
+
+
@@ -1017,26 +967,18 @@ export const CreatePage = () => {
return (
{
- handleToolSelect(drawTool)
- }
- : () => null
- }
+ {...(drawTool.active && {
+ onClick: () => handleToolSelect(drawTool)
+ })}
className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${!drawTool.active ? styles.comingSoon : ''}
`}
>
- {drawTool.icon}
+
{drawTool.label}
{drawTool.active ? (
-
+
) : (
-
+
Coming soon
)}
@@ -1050,6 +992,9 @@ export const CreatePage = () => {
)}
}
+ leftIcon={faFileCirclePlus}
+ centerIcon={faFile}
+ rightIcon={faToolbox}
>
{
return (
<>
-
+
{users
.filter((user) => user.role === UserRole.signer)
.map((user, index) => (
-
{users
.filter((user) => user.role === UserRole.viewer)
- .map((user, index) => {
- const userMeta = metadata[user.pubkey]
+ .map((user) => {
return (
-
-
-
-
-
-
-
-
+
+
)
})}
@@ -1176,23 +1065,26 @@ interface DragItem {
type: string
}
-type SignerRowProps = {
+type CounterpartProps = {
userMeta: ProfileMetadata
user: User
- index: number
- moveSigner: (dragIndex: number, hoverIndex: number) => void
handleUserRoleChange: (role: UserRole, pubkey: string) => void
handleRemoveUser: (pubkey: string) => void
}
-const SignerRow = ({
+type SignerCounterpartProps = CounterpartProps & {
+ index: number
+ moveSigner: (dragIndex: number, hoverIndex: number) => void
+}
+
+const SignerCounterpart = ({
userMeta,
user,
index,
moveSigner,
handleUserRoleChange,
handleRemoveUser
-}: SignerRowProps) => {
+}: SignerCounterpartProps) => {
const ref = useRef
(null)
const [{ handlerId }, drop] = useDrop<
@@ -1266,7 +1158,7 @@ const SignerRow = ({
})
})
- const opacity = isDragging ? 0 : 1
+ const opacity = isDragging ? 0.2 : 1
drag(drop(ref))
return (
@@ -1277,6 +1169,24 @@ const SignerRow = ({
ref={ref}
>
+
+
+ )
+}
+
+const Counterpart = ({
+ userMeta,
+ user,
+ handleUserRoleChange,
+ handleRemoveUser
+}: CounterpartProps) => {
+ return (
+ <>
-
-
+
-
+
+
+
+ >
)
}
diff --git a/src/pages/create/style.module.scss b/src/pages/create/style.module.scss
index 2a1a1d8..a7dd7f2 100644
--- a/src/pages/create/style.module.scss
+++ b/src/pages/create/style.module.scss
@@ -4,6 +4,8 @@
display: flex;
flex-direction: column;
gap: 15px;
+
+ container-type: inline-size;
}
.orderedFilesList {
@@ -40,6 +42,7 @@
}
button {
+ min-width: 44px;
color: $primary-main;
}
@@ -67,10 +70,6 @@
display: flex;
flex-direction: column;
gap: 15px;
-
- // Automatic scrolling if paper-group gets large enough
- // used for files on the left and users on the right
- max-height: 350px;
overflow-x: hidden;
overflow-y: auto;
}
@@ -78,8 +77,9 @@
.inputWrapper {
display: flex;
align-items: center;
+ flex-shrink: 0;
- height: 34px;
+ height: 36px;
overflow: hidden;
border-radius: 4px;
outline: solid 1px #dddddd;
@@ -90,6 +90,43 @@
&:focus-within {
outline-color: $primary-main;
}
+
+ // Override default MUI input styles only inside inputWrapepr
+ :global {
+ .MuiInputBase-input {
+ padding: 7px 14px;
+ }
+ .MuiOutlinedInput-notchedOutline {
+ display: none;
+ }
+ }
+}
+
+.addCounterpart {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: start;
+ gap: 10px;
+
+ > .inputWrapper {
+ flex-shrink: 1;
+ }
+
+ button {
+ min-width: 44px;
+ padding: 11px 12px;
+ }
+}
+
+.users {
+ flex-shrink: 0;
+ max-height: 33vh;
+
+ .counterpartToggleButton {
+ min-width: 44px;
+ padding: 11px 12px;
+ }
}
.user {
@@ -104,6 +141,22 @@
a:hover {
text-decoration: none;
}
+
+ // Higher specificify to override default button styles
+ .counterpartRowToggleButton {
+ min-width: 34px;
+ height: 34px;
+ padding: 0;
+ }
+}
+
+.counterpartRowToggleButton {
+ &[data-variant='primary'] {
+ color: $primary-main;
+ }
+ &[data-variant='secondary'] {
+ color: rgba(0, 0, 0, 0.35);
+ }
}
.avatar {
@@ -130,26 +183,35 @@
.toolbox {
display: grid;
- grid-template-columns: 1fr 1fr 1fr;
+ grid-template-columns: 1fr;
+
+ @container (min-width: 204px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ @container (min-width: 309px) {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
gap: 15px;
- max-height: 450px;
overflow-x: hidden;
overflow-y: auto;
+
+ container-type: inline-size;
}
.toolItem {
- width: 90px;
- height: 90px;
-
transition: ease 0.2s;
- display: inline-flex;
+ display: flex;
flex-direction: column;
gap: 5px;
border-radius: 4px;
padding: 10px 5px 5px 5px;
background: rgba(0, 0, 0, 0.05);
color: rgba(0, 0, 0, 0.5);
+
+ text-align: center;
align-items: center;
justify-content: center;
font-size: 14px;
@@ -162,7 +224,7 @@
color: white;
}
- &:not(.selected) {
+ &:not(.selected, .comingSoon) {
&:hover {
background: $primary-light;
color: white;
@@ -174,3 +236,7 @@
cursor: not-allowed;
}
}
+
+.comingSoonPlaceholder {
+ font-size: 10px;
+}
diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx
index ddc777e..c93c9f8 100644
--- a/src/pages/home/index.tsx
+++ b/src/pages/home/index.tsx
@@ -18,6 +18,7 @@ import {
SigitCardDisplayInfo,
SigitStatus
} from '../../utils'
+import { Footer } from '../../components/Footer/Footer'
// Unsupported Filter options are commented
const FILTERS = [
@@ -262,6 +263,7 @@ export const HomePage = () => {
))}
+
)
}
diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx
index 015d721..deae096 100644
--- a/src/pages/landing/index.tsx
+++ b/src/pages/landing/index.tsx
@@ -19,6 +19,7 @@ import {
faWifi
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack'
+import { Footer } from '../../components/Footer/Footer'
export const LandingPage = () => {
const navigate = useNavigate()
@@ -162,6 +163,7 @@ export const LandingPage = () => {
+
)
}
diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx
index a7b205b..ca4bb87 100644
--- a/src/pages/profile/index.tsx
+++ b/src/pages/profile/index.tsx
@@ -20,6 +20,7 @@ import {
} from '../../utils'
import styles from './style.module.scss'
import { Container } from '../../components/Container'
+import { Footer } from '../../components/Footer/Footer'
export const ProfilePage = () => {
const navigate = useNavigate()
@@ -41,6 +42,16 @@ export const ProfilePage = () => {
const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc] = useState('Fetching metadata')
+ const profileName =
+ pubkey &&
+ profileMetadata &&
+ truncate(
+ profileMetadata.display_name || profileMetadata.name || hexToNpub(pubkey),
+ {
+ length: 16
+ }
+ )
+
useEffect(() => {
if (npub) {
try {
@@ -165,7 +176,10 @@ export const ProfilePage = () => {
className={`${styles.banner} ${!profileMetadata || !profileMetadata.banner ? styles.noImage : ''}`}
>
{profileMetadata && profileMetadata.banner ? (
-
+
) : (
''
)}
@@ -185,6 +199,7 @@ export const ProfilePage = () => {
@@ -224,14 +239,7 @@ export const ProfilePage = () => {
variant="h6"
className={styles.bold}
>
- {truncate(
- profileMetadata.display_name ||
- profileMetadata.name ||
- hexToNpub(pubkey),
- {
- length: 16
- }
- )}
+ {profileName}
)}
@@ -285,6 +293,7 @@ export const ProfilePage = () => {
)}
+
>
)
}
diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx
index b08ea51..599bb90 100644
--- a/src/pages/settings/Settings.tsx
+++ b/src/pages/settings/Settings.tsx
@@ -13,6 +13,7 @@ import { useNavigate } from 'react-router-dom'
import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes'
import { State } from '../../store/rootReducer'
import { Container } from '../../components/Container'
+import { Footer } from '../../components/Footer/Footer'
export const SettingsPage = () => {
const theme = useTheme()
@@ -43,56 +44,59 @@ export const SettingsPage = () => {
}
return (
-
-
+
+
+ Settings
+
+ }
+ >
+ {
+ navigate(getProfileSettingsRoute(usersPubkey!))
}}
>
- Settings
-
- }
- >
- {
- navigate(getProfileSettingsRoute(usersPubkey!))
- }}
- >
-
-
-
- {listItem('Profile')}
-
- {
- navigate(appPrivateRoutes.relays)
- }}
- >
-
-
-
- {listItem('Relays')}
-
- {
- navigate(appPrivateRoutes.cacheSettings)
- }}
- >
-
-
-
- {listItem('Local Cache')}
-
-
-
+
+
+
+ {listItem('Profile')}
+
+ {
+ navigate(appPrivateRoutes.relays)
+ }}
+ >
+
+
+
+ {listItem('Relays')}
+
+ {
+ navigate(appPrivateRoutes.cacheSettings)
+ }}
+ >
+
+
+
+ {listItem('Local Cache')}
+
+
+
+
+ >
)
}
diff --git a/src/pages/settings/cache/index.tsx b/src/pages/settings/cache/index.tsx
index 3d74f0e..fde92a9 100644
--- a/src/pages/settings/cache/index.tsx
+++ b/src/pages/settings/cache/index.tsx
@@ -14,6 +14,7 @@ import { toast } from 'react-toastify'
import { localCache } from '../../../services'
import { LoadingSpinner } from '../../../components/LoadingSpinner'
import { Container } from '../../../components/Container'
+import { Footer } from '../../../components/Footer/Footer'
export const CacheSettingsPage = () => {
const theme = useTheme()
@@ -50,48 +51,51 @@ export const CacheSettingsPage = () => {
}
return (
-
- {isLoading && }
-
- Cache Setting
-
- }
- >
-
-
-
-
- {listItem('Export (coming soon)')}
-
+ <>
+
+ {isLoading && }
+
+ Cache Setting
+
+ }
+ >
+
+
+
+
+ {listItem('Export (coming soon)')}
+
-
-
-
-
- {listItem('Import (coming soon)')}
-
+
+
+
+
+ {listItem('Import (coming soon)')}
+
-
-
-
-
- {listItem('Clear Cache')}
-
-
-
+
+
+
+
+ {listItem('Clear Cache')}
+
+
+
+
+ >
)
}
diff --git a/src/pages/settings/profile/index.tsx b/src/pages/settings/profile/index.tsx
index 8723c2e..9b01a72 100644
--- a/src/pages/settings/profile/index.tsx
+++ b/src/pages/settings/profile/index.tsx
@@ -32,6 +32,7 @@ import {
unixNow
} from '../../../utils'
import { Container } from '../../../components/Container'
+import { Footer } from '../../../components/Footer/Footer'
export const ProfileSettingsPage = () => {
const theme = useTheme()
@@ -385,6 +386,7 @@ export const ProfileSettingsPage = () => {
)}
+
>
)
}
diff --git a/src/pages/settings/relays/index.tsx b/src/pages/settings/relays/index.tsx
index 7e86964..4ed5d62 100644
--- a/src/pages/settings/relays/index.tsx
+++ b/src/pages/settings/relays/index.tsx
@@ -27,6 +27,7 @@ import {
shorten
} from '../../../utils'
import styles from './style.module.scss'
+import { Footer } from '../../../components/Footer/Footer'
export const RelaysPage = () => {
const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey)
@@ -270,161 +271,164 @@ const RelayItem = ({
})
return (
-
-
-
-
- {relayInfo &&
- relayInfo.limitation &&
- relayInfo.limitation?.payment_required && (
-
- setDisplayRelayInfo((prev) => !prev)}
- />
-
- )}
+ <>
+
+
+
+
+ {relayInfo &&
+ relayInfo.limitation &&
+ relayInfo.limitation?.payment_required && (
+
+ setDisplayRelayInfo((prev) => !prev)}
+ />
+
+ )}
-
+
- handleLeaveRelay(relayURI)}
- >
-
- Leave
-
-
-
-
- setDisplayRelayInfo((prev) => !prev)}
- className={styles.showInfo}
- >
- Show info{' '}
- {displayRelayInfo ? (
-
- ) : (
-
- )}
-
- ) : (
- ''
- )
- }
- />
- handleRelayWriteChange(relayURI, event)}
- />
-
- {displayRelayInfo && (
- <>
-
-
-
- {relayInfo &&
- Object.keys(relayInfo).map((key: string) => {
- const infoTitle = capitalizeFirstLetter(
- key.replace('_', ' ')
- )
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- let infoValue = (relayInfo as any)[key]
+ handleLeaveRelay(relayURI)}
+ >
+
+ Leave
+
+
+
+
+ setDisplayRelayInfo((prev) => !prev)}
+ className={styles.showInfo}
+ >
+ Show info{' '}
+ {displayRelayInfo ? (
+
+ ) : (
+
+ )}
+
+ ) : (
+ ''
+ )
+ }
+ />
+ handleRelayWriteChange(relayURI, event)}
+ />
+
+ {displayRelayInfo && (
+ <>
+
+
+
+ {relayInfo &&
+ Object.keys(relayInfo).map((key: string) => {
+ const infoTitle = capitalizeFirstLetter(
+ key.replace('_', ' ')
+ )
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let infoValue = (relayInfo as any)[key]
- switch (key) {
- case 'pubkey':
- infoValue = shorten(hexToNpub(infoValue), 15)
+ switch (key) {
+ case 'pubkey':
+ infoValue = shorten(hexToNpub(infoValue), 15)
- break
+ break
- case 'limitation':
- infoValue = (
-
- {Object.keys(infoValue).map((valueKey) => (
- -
-
- {capitalizeFirstLetter(
- valueKey.split('_').join(' ')
- )}
- :
- {' '}
- {`${infoValue[valueKey]}`}
-
- ))}
-
- )
+ case 'limitation':
+ infoValue = (
+
+ {Object.keys(infoValue).map((valueKey) => (
+ -
+
+ {capitalizeFirstLetter(
+ valueKey.split('_').join(' ')
+ )}
+ :
+ {' '}
+ {`${infoValue[valueKey]}`}
+
+ ))}
+
+ )
- break
+ break
- case 'fees':
- infoValue = (
-
- {Object.keys(infoValue).map((valueKey) => (
- -
-
- {capitalizeFirstLetter(
- valueKey.split('_').join(' ')
- )}
- :
- {' '}
- {`${infoValue[valueKey].map((fee: RelayFee) => `${fee.amount} ${fee.unit}`)}`}
-
- ))}
-
- )
- break
- default:
- break
- }
+ case 'fees':
+ infoValue = (
+
+ {Object.keys(infoValue).map((valueKey) => (
+ -
+
+ {capitalizeFirstLetter(
+ valueKey.split('_').join(' ')
+ )}
+ :
+ {' '}
+ {`${infoValue[valueKey].map((fee: RelayFee) => `${fee.amount} ${fee.unit}`)}`}
+
+ ))}
+
+ )
+ break
+ default:
+ break
+ }
- if (Array.isArray(infoValue)) {
- infoValue = infoValue.join(', ')
- }
+ if (Array.isArray(infoValue)) {
+ infoValue = infoValue.join(', ')
+ }
- return (
-
-
- {infoTitle}:
- {' '}
- {infoValue}
- {key === 'pubkey' ? (
- {
- navigator.clipboard.writeText(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- hexToNpub((relayInfo as any)[key])
- )
+ return (
+
+
+ {infoTitle}:
+ {' '}
+ {infoValue}
+ {key === 'pubkey' ? (
+ {
+ navigator.clipboard.writeText(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ hexToNpub((relayInfo as any)[key])
+ )
- toast.success('Copied to clipboard', {
- autoClose: 1000,
- hideProgressBar: true
- })
- }}
- />
- ) : null}
-
- )
- })}
-
-
- >
- )}
-
-
+ toast.success('Copied to clipboard', {
+ autoClose: 1000,
+ hideProgressBar: true
+ })
+ }}
+ />
+ ) : null}
+
+ )
+ })}
+
+
+ >
+ )}
+
+
+
+ >
)
}
diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx
index b8cfe5c..8720954 100644
--- a/src/pages/sign/index.tsx
+++ b/src/pages/sign/index.tsx
@@ -38,10 +38,8 @@ import {
import { Container } from '../../components/Container'
import { DisplayMeta } from './internal/displayMeta'
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 { getLastSignersSig } from '../../utils/sign.ts'
+import { getLastSignersSig, isFullySigned } from '../../utils/sign.ts'
import {
filterMarksByPubkey,
getCurrentUserMarks,
@@ -49,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,
@@ -76,7 +78,7 @@ export const SignPage = () => {
const [selectedFile, setSelectedFile] = useState
(null)
- const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({})
+ const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
@@ -110,13 +112,13 @@ export const SignPage = () => {
const [currentUserMarks, setCurrentUserMarks] = useState(
[]
)
- const [isReadyToSign, setIsReadyToSign] = useState(false)
+ const [isMarksCompleted, setIsMarksCompleted] = useState(false)
const [otherUserMarks, setOtherUserMarks] = useState([])
useEffect(() => {
if (signers.length > 0) {
// check if all signers have signed then its fully signed
- if (signers.every((signer) => signedBy.includes(signer))) {
+ if (isFullySigned(signers, signedBy)) {
setSignedStatus(SignedStatus.Fully_Signed)
} else {
for (const signer of signers) {
@@ -214,7 +216,7 @@ export const SignPage = () => {
const otherUserMarks = findOtherUserMarks(signedMarks, usersPubkey!)
setOtherUserMarks(otherUserMarks)
setCurrentUserMarks(currentUserMarks)
- setIsReadyToSign(isCurrentUserMarksComplete(currentUserMarks))
+ setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks))
}
setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[])
@@ -402,7 +404,7 @@ export const SignPage = () => {
return
}
- const files: { [filename: string]: PdfFile } = {}
+ const files: { [filename: string]: SigitFile } = {}
const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files).map((entry) => entry.name)
@@ -416,8 +418,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) {
fileHashes[fileName] = hash
@@ -462,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)
@@ -479,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) {
@@ -541,10 +542,13 @@ export const SignPage = () => {
setLoadingSpinnerDesc('Signing nostr event')
const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!))
- if (!prevSig) return
+ if (!prevSig) {
+ setIsLoading(false)
+ toast.error('Previous signature is invalid')
+ return
+ }
- const marks = getSignerMarksForMeta()
- if (!marks) return
+ const marks = getSignerMarksForMeta() || []
const signedEvent = await signEventForMeta({ prevSig, marks })
if (!signedEvent) return
@@ -764,8 +768,8 @@ export const SignPage = () => {
zip.file('meta.json', stringifiedMeta)
- for (const [fileName, pdf] of Object.entries(files)) {
- zip.file(`files/${fileName}`, await pdf.file.arrayBuffer())
+ for (const [fileName, file] of Object.entries(files)) {
+ zip.file(`files/${fileName}`, await file.arrayBuffer())
}
const arrayBuffer = await zip
@@ -802,8 +806,8 @@ export const SignPage = () => {
zip.file('meta.json', stringifiedMeta)
- for (const [fileName, pdf] of Object.entries(files)) {
- zip.file(`files/${fileName}`, await pdf.file.arrayBuffer())
+ for (const [fileName, file] of Object.entries(files)) {
+ zip.file(`files/${fileName}`, await file.arrayBuffer())
}
const arrayBuffer = await zip
@@ -882,90 +886,90 @@ export const SignPage = () => {
return
}
- if (isReadyToSign) {
+ if (!isMarksCompleted && signedStatus === SignedStatus.User_Is_Next_Signer) {
return (
- <>
-
- {displayInput && (
- <>
-
- Select sigit file
-
-
-
- setSelectedFile(value)}
- />
-
-
- {selectedFile && (
-
-
-
- )}
- >
- )}
-
- {submittedBy && Object.entries(files).length > 0 && meta && (
- <>
-
-
- {signedStatus === SignedStatus.Fully_Signed && (
-
-
-
- )}
-
- {signedStatus === SignedStatus.User_Is_Next_Signer && (
-
-
-
- )}
-
- {isSignerOrCreator && (
-
-
-
- )}
- >
- )}
-
- >
+
)
}
return (
-
+ <>
+
+ {displayInput && (
+ <>
+
+ Select sigit file
+
+
+
+ setSelectedFile(value)}
+ />
+
+
+ {selectedFile && (
+
+
+
+ )}
+ >
+ )}
+
+ {submittedBy && Object.entries(files).length > 0 && meta && (
+ <>
+
+
+ {signedStatus === SignedStatus.Fully_Signed && (
+
+
+
+ )}
+
+ {signedStatus === SignedStatus.User_Is_Next_Signer && (
+
+
+
+ )}
+
+ {isSignerOrCreator && (
+
+
+
+ )}
+ >
+ )}
+
+ >
)
}
diff --git a/src/pages/sign/internal/displayMeta.tsx b/src/pages/sign/internal/displayMeta.tsx
index 03ba364..bb298c6 100644
--- a/src/pages/sign/internal/displayMeta.tsx
+++ b/src/pages/sign/internal/displayMeta.tsx
@@ -34,11 +34,11 @@ import { UserAvatar } from '../../../components/UserAvatar'
import { MetadataController } from '../../../controllers'
import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils'
import styles from '../style.module.scss'
-import { PdfFile } from '../../../types/drawing.ts'
+import { SigitFile } from '../../../utils/file'
type DisplayMetaProps = {
meta: Meta
- files: { [filename: string]: PdfFile }
+ files: { [fileName: string]: SigitFile }
submittedBy: string
signers: `npub1${string}`[]
viewers: `npub1${string}`[]
@@ -143,12 +143,9 @@ export const DisplayMeta = ({
})
}, [users, submittedBy, metadata])
- const downloadFile = async (filename: string) => {
- const arrayBuffer = await files[filename].file.arrayBuffer()
- if (!arrayBuffer) return
-
- const blob = new Blob([arrayBuffer])
- saveAs(blob, filename)
+ const downloadFile = async (fileName: string) => {
+ const file = files[fileName]
+ saveAs(file)
}
return (
diff --git a/src/pages/sign/style.module.scss b/src/pages/sign/style.module.scss
index 1dbc6c5..dffb039 100644
--- a/src/pages/sign/style.module.scss
+++ b/src/pages/sign/style.module.scss
@@ -2,8 +2,6 @@
.container {
color: $text-color;
- //width: 550px;
- //max-width: 550px;
.inputBlock {
position: relative;
@@ -67,7 +65,7 @@
//z-index: 200;
}
- .fixedBottomForm input[type="text"] {
+ .fixedBottomForm input[type='text'] {
width: 80%;
padding: 10px;
font-size: 16px;
diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx
index c67c2fc..e869c98 100644
--- a/src/pages/verify/index.tsx
+++ b/src/pages/verify/index.tsx
@@ -1,4 +1,4 @@
-import { Box, Button, Divider, Tooltip, Typography } from '@mui/material'
+import { Box, Button, Tooltip, Typography } from '@mui/material'
import JSZip from 'jszip'
import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools'
@@ -26,11 +26,11 @@ import {
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,
+ FONT_SIZE,
+ FONT_TYPE,
groupMarksByFileNamePage,
inPx
} from '../../utils/pdf.ts'
@@ -49,6 +49,15 @@ 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'
+import { useScale } from '../../hooks/useScale.tsx'
+import {
+ faCircleInfo,
+ faFile,
+ faFileDownload
+} from '@fortawesome/free-solid-svg-icons'
interface PdfViewProps {
files: CurrentUserFile[]
@@ -64,77 +73,90 @@ const SlimPdfView = ({
parsedSignatureEvents
}: PdfViewProps) => {
const pdfRefs = useRef<(HTMLDivElement | null)[]>([])
+ const { from } = useScale()
useEffect(() => {
if (currentFile !== null && !!pdfRefs.current[currentFile.id]) {
pdfRefs.current[currentFile.id]?.scrollIntoView({
- behavior: 'smooth',
- block: 'end'
+ behavior: 'smooth'
})
}
}, [currentFile])
return (
-
- {files.map((currentUserFile, i) => {
- const { hash, filename, pdfFile, id } = currentUserFile
- const signatureEvents = Object.keys(parsedSignatureEvents)
- if (!hash) return
- return (
-
- (pdfRefs.current[id] = el)}
- className={styles.fileWrapper}
- >
- {pdfFile.pages.map((page, i) => {
- const marks: Mark[] = []
-
- signatureEvents.forEach((e) => {
- const m = parsedSignatureEvents[
- e as `npub1${string}`
- ].parsedContent?.marks.filter(
- (m) => m.pdfFileHash == hash && m.location.page == i
- )
- if (m) {
- marks.push(...m)
- }
- })
- return (
-
-
- {marks.map((m) => {
- return (
-
- {m.value}
-
- )
- })}
-
- )
- })}
-
-
- {i < files.length - 1 && (
-
+ {files.length > 0 ? (
+ files.map((currentUserFile, i) => {
+ const { hash, file, id } = currentUserFile
+ const signatureEvents = Object.keys(parsedSignatureEvents)
+ if (!hash) return
+ return (
+
+ (pdfRefs.current[id] = el)}
+ className="file-wrapper"
>
- File Separator
-
- )}
-
- )
- })}
+ {file.isPdf &&
+ file.pages?.map((page, i) => {
+ const marks: Mark[] = []
+
+ signatureEvents.forEach((e) => {
+ const m = parsedSignatureEvents[
+ e as `npub1${string}`
+ ].parsedContent?.marks.filter(
+ (m) => m.pdfFileHash == hash && m.location.page == i
+ )
+ if (m) {
+ marks.push(...m)
+ }
+ })
+ return (
+
+
+ {marks.map((m) => {
+ return (
+
+ {m.value}
+
+ )
+ })}
+
+ )
+ })}
+ {file.isImage && (
+
+ )}
+ {!(file.isPdf || file.isImage) && (
+
+ )}
+
+ {i < files.length - 1 && }
+
+ )
+ })
+ ) : (
+
+ )}
)
}
@@ -171,7 +193,7 @@ export const VerifyPage = () => {
const [currentFileHashes, setCurrentFileHashes] = useState<{
[key: string]: string | null
}>({})
- const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({})
+ const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
const [currentFile, setCurrentFile] = useState(null)
const [signatureFileHashes, setSignatureFileHashes] = useState<{
[key: string]: string
@@ -230,7 +252,7 @@ export const VerifyPage = () => {
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).map(
(entry) => entry.name
@@ -246,7 +268,7 @@ export const VerifyPage = () => {
)
if (arrayBuffer) {
- files[fileName] = await convertToPdfFile(
+ files[fileName] = await convertToSigitFile(
arrayBuffer,
fileName!
)
@@ -423,10 +445,15 @@ export const VerifyPage = () => {
const marks = extractMarksFromSignedMeta(updatedMeta)
const marksByPage = groupMarksByFileNamePage(marks)
- for (const [fileName, pdf] of Object.entries(files)) {
- const pages = await addMarks(pdf.file, marksByPage[fileName])
- const blob = await convertToPdfBlob(pages)
- zip.file(`files/${fileName}`, blob)
+ 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 {
+ zip.file(`files/${fileName}`, file)
+ }
}
const arrayBuffer = await zip
@@ -545,6 +572,9 @@ export const VerifyPage = () => {
>
}
right={}
+ leftIcon={faFileDownload}
+ centerIcon={faFile}
+ rightIcon={faCircleInfo}
>
=> {
const zip = new JSZip()
const marks = extractMarksFromSignedMeta(meta)
const marksByFileNamePage = groupMarksByFileNamePage(marks)
- for (const [fileName, pdf] of Object.entries(files)) {
- const pages = await addMarks(pdf.file, marksByFileNamePage[fileName])
- const blob = await convertToPdfBlob(pages)
- zip.file(`files/${fileName}`, blob)
+ for (const [fileName, file] of Object.entries(files)) {
+ if (file.isPdf) {
+ // Handle PDF Files
+ const pages = await addMarks(file, marksByFileNamePage[fileName])
+ const blob = await convertToPdfBlob(pages)
+ zip.file(`files/${fileName}`, blob)
+ } else {
+ // Handle other files
+ zip.file(`files/${fileName}`, file)
+ }
}
return zip
}
-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
+ // - generate ObjectRL for image files
+ await sigitFile.process()
+ return sigitFile
+}
+
+/**
+ * Takes an ArrayBuffer and converts to Sigit's Internal File type
+ * @param arrayBuffer
+ * @param fileName
+ */
+export const convertToSigitFile = async (
+ arrayBuffer: ArrayBuffer,
+ fileName: string
+): Promise => {
+ const type = getMediaType(extractFileExtension(fileName))
+ const file = toFile(arrayBuffer, fileName, type)
+ const sigitFile = await getSigitFile(file)
+ return sigitFile
+}
+
+/**
+ * @param fileNames - List of filenames to check
+ * @returns List of extensions and if all are same
+ */
+export const extractFileExtensions = (fileNames: string[]) => {
+ const extensions = fileNames.reduce((result: string[], file: string) => {
+ const extension = file.split('.').pop()
+ if (extension) {
+ result.push(extension)
+ }
+ return result
+ }, [])
+
+ const isSame = extensions.every((ext) => ext === extensions[0])
+
+ return { extensions, isSame }
+}
+
+/**
+ * @param fileName - Filename to check
+ * @returns Extension string
+ */
+export const extractFileExtension = (fileName: string) => {
+ const parts = fileName.split('.')
+ return parts[parts.length - 1]
+}
+
+export const getMediaType = (extension: string) => {
+ return MOST_COMMON_MEDIA_TYPES.get(extension)
+}
+
+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())
+}
diff --git a/src/utils/mark.ts b/src/utils/mark.ts
index effa2ba..44540c4 100644
--- a/src/utils/mark.ts
+++ b/src/utils/mark.ts
@@ -47,7 +47,7 @@ const filterMarksByPubkey = (marks: Mark[], pubkey: string): Mark[] => {
/**
* Takes Signed Doc Signatures part of Meta and extracts
- * all Marks into one flar array, regardless of the user.
+ * all Marks into one flat array, regardless of the user.
* @param meta
*/
const extractMarksFromSignedMeta = (meta: Meta): Mark[] => {
diff --git a/src/utils/meta.ts b/src/utils/meta.ts
index 0bee969..c915f66 100644
--- a/src/utils/meta.ts
+++ b/src/utils/meta.ts
@@ -2,6 +2,12 @@ import { CreateSignatureEventContent, Meta } from '../types'
import { fromUnixTimestamp, parseJson } from '.'
import { Event, verifyEvent } from 'nostr-tools'
import { toast } from 'react-toastify'
+import { extractFileExtensions } from './file'
+import { handleError } from '../types/errors'
+import {
+ MetaParseError,
+ MetaParseErrorType
+} from '../types/errors/MetaParseError'
export enum SignStatus {
Signed = 'Signed',
@@ -16,58 +22,6 @@ export enum SigitStatus {
Complete = 'Completed'
}
-type Jsonable =
- | string
- | number
- | boolean
- | null
- | undefined
- | readonly Jsonable[]
- | { readonly [key: string]: Jsonable }
- | { toJSON(): Jsonable }
-
-export class SigitMetaParseError extends Error {
- public readonly context?: Jsonable
-
- constructor(
- message: string,
- options: { cause?: Error; context?: Jsonable } = {}
- ) {
- const { cause, context } = options
-
- super(message, { cause })
- this.name = this.constructor.name
-
- this.context = context
- }
-}
-
-/**
- * Handle meta errors
- * Wraps the errors without message property and stringify to a message so we can use it later
- * @param error
- * @returns
- */
-function handleError(error: unknown): Error {
- if (error instanceof Error) return error
-
- // No message error, wrap it and stringify
- let stringified = 'Unable to stringify the thrown value'
- try {
- stringified = JSON.stringify(error)
- } catch (error) {
- console.error(stringified, error)
- }
-
- return new Error(`[SiGit Error]: ${stringified}`)
-}
-
-// Reuse common error messages for meta parsing
-export enum SigitMetaParseErrorType {
- 'PARSE_ERROR_EVENT' = 'error occurred in parsing the create signature event',
- 'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content"
-}
-
export interface SigitCardDisplayInfo {
createdAt?: number
title?: string
@@ -88,7 +42,7 @@ export const parseNostrEvent = async (raw: string): Promise => {
const event = await parseJson(raw)
return event
} catch (error) {
- throw new SigitMetaParseError(SigitMetaParseErrorType.PARSE_ERROR_EVENT, {
+ throw new MetaParseError(MetaParseErrorType.PARSE_ERROR_EVENT, {
cause: handleError(error),
context: raw
})
@@ -108,8 +62,8 @@ export const parseCreateSignatureEventContent = async (
await parseJson(raw)
return createSignatureEventContent
} catch (error) {
- throw new SigitMetaParseError(
- SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT,
+ throw new MetaParseError(
+ MetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT,
{
cause: handleError(error),
context: raw
@@ -164,7 +118,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
return sigitInfo
} catch (error) {
- if (error instanceof SigitMetaParseError) {
+ if (error instanceof MetaParseError) {
toast.error(error.message)
console.error(error.name, error.message, error.cause, error.context)
} else {
@@ -172,29 +126,3 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
}
}
}
-
-/**
- * @param fileNames - List of filenames to check
- * @returns List of extensions and if all are same
- */
-export const extractFileExtensions = (fileNames: string[]) => {
- const extensions = fileNames.reduce((result: string[], file: string) => {
- const extension = file.split('.').pop()
- if (extension) {
- result.push(extension)
- }
- return result
- }, [])
-
- const isSame = extensions.every((ext) => ext === extensions[0])
-
- return { extensions, isSame }
-}
-
-/**
- * @param fileName - Filename to check
- * @returns Extension string
- */
-export const extractFileExtension = (fileName: string) => {
- return fileName.split('.').pop()
-}
diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts
index 9bade2f..eceb8d8 100644
--- a/src/utils/nostr.ts
+++ b/src/utils/nostr.ts
@@ -11,6 +11,7 @@ import {
getEventHash,
getPublicKey,
kinds,
+ nip04,
nip19,
nip44,
verifyEvent
@@ -30,10 +31,26 @@ import { AuthState, Keys } from '../store/auth/types'
import { RelaysState } from '../store/relays/types'
import store from '../store/store'
import { Meta, SignedEvent, UserAppData } from '../types'
-import { getHash } from './hash'
+import { getDefaultRelayMap } from './relays'
import { parseJson, removeLeadingSlash } from './string'
import { timeout } from './utils'
-import { getDefaultRelayMap } from './relays'
+import { getHash } from './hash'
+
+/**
+ * Generates a `d` tag for userAppData
+ */
+const getDTagForUserAppData = async (): Promise => {
+ const isLoggedIn = store.getState().auth?.loggedIn
+ const pubkey = store.getState().auth?.usersPubkey
+
+ if (!isLoggedIn || !pubkey) {
+ throw new Error(
+ 'For generating d tag user must be logged in and a valid pubkey should exists in app Store'
+ )
+ }
+
+ return getHash(`938_${pubkey}`)
+}
/**
* @param hexKey hex private or public key
@@ -377,13 +394,13 @@ export const getUsersAppData = async (): Promise => {
}
// Generate an identifier for the user's nip78
- const hash = await getHash('938' + usersPubkey)
- if (!hash) return null
+ const dTag = await getDTagForUserAppData()
+ if (!dTag) return null
// Define a filter for fetching events
const filter: Filter = {
kinds: [kinds.Application],
- '#d': [hash]
+ '#d': [dTag]
}
const encryptedContent = await relayController
@@ -578,14 +595,14 @@ export const updateUsersAppData = async (meta: Meta) => {
if (!encryptedContent) return null
// generate the identifier for user's appData event
- const hash = await getHash('938' + usersPubkey)
- if (!hash) return null
+ const dTag = await getDTagForUserAppData()
+ if (!dTag) return null
const updatedEvent: UnsignedEvent = {
kind: kinds.Application,
pubkey: usersPubkey!,
created_at: unixNow(),
- tags: [['d', hash]],
+ tags: [['d', dTag]],
content: encryptedContent
}
@@ -693,9 +710,10 @@ const uploadUserAppDataToBlossom = async (
// Convert the private key from hex to bytes
const secretKey = hexToBytes(privateKey)
// Encrypt the JSON string using the secret key
- const encrypted = nip44.v2.encrypt(
- stringified,
- nip44ConversationKey(secretKey, getPublicKey(secretKey))
+ const encrypted = await nip04.encrypt(
+ secretKey,
+ getPublicKey(secretKey),
+ stringified
)
// Create a blob from the encrypted data
@@ -788,10 +806,7 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
const pubkey = getPublicKey(secret)
// Decrypt the encrypted data using the secret and public key
- const decrypted = nip44.v2.decrypt(
- encrypted,
- nip44ConversationKey(secret, pubkey)
- )
+ const decrypted = await nip04.decrypt(secret, pubkey, encrypted)
// Parse the decrypted JSON content
const parsedContent = await parseJson<{
diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts
index ce2f132..763d582 100644
--- a/src/utils/pdf.ts
+++ b/src/utils/pdf.ts
@@ -1,18 +1,15 @@
-import { PdfFile, PdfPage } from '../types/drawing.ts'
-import * as PDFJS from 'pdfjs-dist'
+import { PdfPage } from '../types/drawing.ts'
import { PDFDocument } from 'pdf-lib'
import { Mark } from '../types/mark.ts'
-PDFJS.GlobalWorkerOptions.workerSrc = new URL(
- 'pdfjs-dist/build/pdf.worker.min.mjs',
- import.meta.url
-).toString()
+import * as PDFJS from 'pdfjs-dist'
+import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker'
+if (!PDFJS.GlobalWorkerOptions.workerPort) {
+ // Use workerPort and allow worker to be shared between all getDocument calls
+ const worker = new PDFJSWorker()
+ PDFJS.GlobalWorkerOptions.workerPort = worker
+}
-/**
- * Scale between the PDF page's natural size and rendered size
- * @constant {number}
- */
-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,58 +17,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 = 16
/**
* 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 => {
- const data = await readPdf(file)
- const pages = await pdfToImages(data)
- return { file, pages, expanded: false }
-}
-/**
- * Transforms an array of generic PDF Files into an array of Sigit's
- * internal representation of Pdf Files
- * @param selectedFiles - an array of generic PDF Files
- * @return PdfFile[] - an array of Sigit's internal Pdf File type
- */
-const toPdfFiles = async (selectedFiles: File[]): Promise => {
- return Promise.all(selectedFiles.filter(isPdf).map(toPdfFile))
-}
-
-/**
- * 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 pixel string
* @param coordinate
*/
-const inPx = (coordinate: number): string => `${coordinate}px`
+export const inPx = (coordinate: number): string => `${coordinate}px`
/**
* A utility that checks if a given file is of the pdf type
* @param file
*/
-const isPdf = (file: File) => file.type.toLowerCase().includes('pdf')
+export const isPdf = (file: File) => file.type.toLowerCase().includes('pdf')
/**
* Reads the pdf file binaries
*/
-const readPdf = (file: File): Promise => {
+export const readPdf = (file: File): Promise => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
@@ -95,33 +62,55 @@ const readPdf = (file: File): Promise => {
})
}
+export const getInnerContentWidth = () => {
+ // Fetch the first container element we find
+ const element = document.querySelector('#content-preview')
+
+ if (element) {
+ const style = getComputedStyle(element)
+
+ // Calculate width without padding
+ const widthWithoutPadding =
+ element.clientWidth - parseFloat(style.padding) * 2
+
+ return widthWithoutPadding
+ }
+
+ // Default value
+ return 620
+}
+
/**
* Converts pdf to the images
* @param data pdf file bytes
*/
-const pdfToImages = async (data: string | ArrayBuffer): Promise => {
- const images: string[] = []
+export const pdfToImages = async (
+ data: string | ArrayBuffer
+): Promise => {
+ const pages: PdfPage[] = []
const pdf = await PDFJS.getDocument(data).promise
const canvas = document.createElement('canvas')
+ const width = getInnerContentWidth()
for (let i = 0; i < pdf.numPages; i++) {
const page = await pdf.getPage(i + 1)
- const viewport = page.getViewport({ scale: SCALE })
+
+ const originalViewport = page.getViewport({ scale: 1 })
+ const scale = width / originalViewport.width
+ const viewport = page.getViewport({ scale: scale })
const context = canvas.getContext('2d')
canvas.height = viewport.height
canvas.width = viewport.width
+
await page.render({ canvasContext: context!, viewport: viewport }).promise
- images.push(canvas.toDataURL())
+ pages.push({
+ image: canvas.toDataURL(),
+ width: originalViewport.width,
+ drawnFields: []
+ })
}
- return Promise.resolve(
- images.map((image) => {
- return {
- image,
- drawnFields: []
- }
- })
- )
+ return pages
}
/**
@@ -129,7 +118,7 @@ const pdfToImages = async (data: string | ArrayBuffer): Promise => {
* Returns an array of encoded images where each image is a representation
* of a PDF page with completed and signed marks from all users
*/
-const addMarks = async (
+export const addMarks = async (
file: File,
marksPerPage: { [key: string]: Mark[] }
) => {
@@ -141,34 +130,39 @@ const addMarks = async (
for (let i = 0; i < pdf.numPages; i++) {
const page = await pdf.getPage(i + 1)
- const viewport = page.getViewport({ scale: SCALE })
+ const viewport = page.getViewport({ scale: 1 })
const context = canvas.getContext('2d')
canvas.height = viewport.height
canvas.width = viewport.width
- await page.render({ canvasContext: context!, viewport: viewport }).promise
+ if (context) {
+ await page.render({ canvasContext: context, viewport: viewport }).promise
- if (marksPerPage && Object.hasOwn(marksPerPage, i))
- marksPerPage[i]?.forEach((mark) => draw(mark, context!))
+ if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
+ marksPerPage[i]?.forEach((mark) => draw(mark, context))
+ }
- images.push(canvas.toDataURL())
+ images.push(canvas.toDataURL())
+ }
}
- return Promise.resolve(images)
+ canvas.remove()
+
+ return images
}
/**
* Utility to scale mark in line with the PDF-to-PNG scale
*/
-const scaleMark = (mark: Mark): Mark => {
+export const scaleMark = (mark: Mark, scale: number): Mark => {
const { location } = mark
return {
...mark,
location: {
...location,
- width: location.width * SCALE,
- height: location.height * SCALE,
- left: location.left * SCALE,
- top: location.top * SCALE
+ width: location.width * scale,
+ height: location.height * scale,
+ left: location.left * scale,
+ top: location.top * scale
}
}
}
@@ -177,29 +171,32 @@ 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
- ctx!.fillStyle = 'black'
- const textMetrics = ctx!.measureText(mark.value!)
+ ctx.font = FONT_SIZE + 'px ' + FONT_TYPE
+ ctx.fillStyle = 'black'
+ const textMetrics = ctx.measureText(mark.value!)
+ const textHeight =
+ textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent
const textX = location.left + (location.width - textMetrics.width) / 2
- const textY = location.top + (location.height + parseInt(ctx!.font)) / 2
- ctx!.fillText(mark.value!, textX, textY)
+ const textY = location.top + (location.height + textHeight) / 2
+ ctx.fillText(mark.value!, textX, textY)
}
/**
* Takes an array of encoded PDF pages and returns a blob that is a complete PDF file
* @param markedPdfPages
*/
-const convertToPdfBlob = async (markedPdfPages: string[]): Promise => {
+export const convertToPdfBlob = async (
+ markedPdfPages: string[]
+): Promise => {
const pdfDoc = await PDFDocument.create()
for (const page of markedPdfPages) {
@@ -217,30 +214,15 @@ const convertToPdfBlob = async (markedPdfPages: string[]): Promise => {
return new Blob([pdfBytes], { type: 'application/pdf' })
}
-/**
- * Takes an ArrayBuffer of a PDF file and converts to Sigit's Internal Pdf File type
- * @param arrayBuffer
- * @param fileName
- */
-const convertToPdfFile = async (
- arrayBuffer: ArrayBuffer,
- fileName: string
-): Promise => {
- const file = toFile(arrayBuffer, fileName)
- return toPdfFile(file)
-}
-
/**
* @param marks - an array of Marks
* @function hasValue removes any Mark without a property
- * @function scaleMark scales remaining marks in line with SCALE
* @function byPage groups remaining Marks by their page marks.location.page
*/
-const groupMarksByFileNamePage = (marks: Mark[]) => {
+export const groupMarksByFileNamePage = (marks: Mark[]) => {
return marks
.filter(hasValue)
- .map(scaleMark)
- .reduce<{ [filename: string]: { [page: number]: Mark[] } }>(byPage, {})
+ .reduce<{ [fileName: string]: { [page: number]: Mark[] } }>(byPage, {})
}
/**
@@ -251,30 +233,19 @@ const groupMarksByFileNamePage = (marks: Mark[]) => {
* @param obj - accumulator in the reducer callback
* @param mark - current value, i.e. Mark being examined
*/
-const byPage = (
+export const byPage = (
obj: { [filename: string]: { [page: number]: Mark[] } },
mark: Mark
) => {
- const filename = mark.fileName
+ const fileName = mark.fileName
const pageNumber = mark.location.page
- const pages = obj[filename] ?? {}
+ const pages = obj[fileName] ?? {}
const marks = pages[pageNumber] ?? []
return {
...obj,
- [filename]: {
+ [fileName]: {
...pages,
[pageNumber]: [...marks, mark]
}
}
}
-
-export {
- toFile,
- toPdfFile,
- toPdfFiles,
- inPx,
- convertToPdfFile,
- addMarks,
- convertToPdfBlob,
- groupMarksByFileNamePage
-}
diff --git a/src/utils/relays.ts b/src/utils/relays.ts
index c38767c..5b79f3d 100644
--- a/src/utils/relays.ts
+++ b/src/utils/relays.ts
@@ -30,7 +30,6 @@ const findRelayListAndUpdateCache = async (
authors: [hexKey]
}
- console.count('findRelayListAndUpdateCache')
const event = await relayController.fetchEvent(eventFilter, lookUpRelays)
if (event) {
await localCache.addUserRelayListMetadata(event)
diff --git a/src/utils/sign.ts b/src/utils/sign.ts
index 3369c54..ff67e44 100644
--- a/src/utils/sign.ts
+++ b/src/utils/sign.ts
@@ -31,4 +31,16 @@ const getLastSignersSig = (
}
}
-export { getLastSignersSig }
+/**
+ * Checks if all signers have signed the sigit
+ * @param signers - an array of npubs of all signers from the Sigit
+ * @param signedBy - an array of npubs that have signed it already
+ */
+const isFullySigned = (
+ signers: `npub1${string}`[],
+ signedBy: `npub1${string}`[]
+): boolean => {
+ return signers.every((signer) => signedBy.includes(signer))
+}
+
+export { getLastSignersSig, isFullySigned }
diff --git a/src/utils/string.ts b/src/utils/string.ts
index 9ac0f05..14eee83 100644
--- a/src/utils/string.ts
+++ b/src/utils/string.ts
@@ -1,5 +1,5 @@
/**
- * Function will replace the middle of the string with 3 dots if length greater then
+ * Function will replace the middle of the string with ellipsis if length greater then
* offset value
* @param str string to shorten
* @param offset of how many chars to keep in the beginning and the end
@@ -9,10 +9,7 @@ export const shorten = (str: string, offset = 9) => {
// return original string if it is not long enough
if (str.length < offset * 2 + 4) return str
- return `${str.slice(0, offset)}...${str.slice(
- str.length - offset,
- str.length
- )}`
+ return `${str.slice(0, offset)}…${str.slice(str.length - offset, str.length)}`
}
export const stringToHex = (str: string) => {
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index bde528b..f32e14e 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -1,5 +1,5 @@
-import { PdfFile } from '../types/drawing.ts'
import { CurrentUserFile } from '../types/file.ts'
+import { SigitFile } from './file.ts'
export const compareObjects = (
obj1: object | null | undefined,
@@ -75,13 +75,13 @@ export const timeout = (ms: number = 60000) => {
* @param creatorFileHashes
*/
export const getCurrentUserFiles = (
- files: { [filename: string]: PdfFile },
+ files: { [filename: string]: SigitFile },
fileHashes: { [key: string]: string | null },
creatorFileHashes: { [key: string]: string }
): CurrentUserFile[] => {
- return Object.entries(files).map(([filename, pdfFile], index) => {
+ return Object.entries(files).map(([filename, file], index) => {
return {
- pdfFile,
+ file,
filename,
id: index + 1,
...(!!fileHashes[filename] && { hash: fileHashes[filename]! }),
@@ -89,3 +89,32 @@ export const getCurrentUserFiles = (
}
})
}
+
+/**
+ * Utility function that generates a promise with a callback on each array item
+ * and retuns only non-null fulfilled results
+ * @param array
+ * @param cb callback that generates a promise
+ * @returns Array with the non-null results
+ */
+export const settleAllFullfilfedPromises = async - (
+ array: Item[],
+ cb: (arg: Item) => Promise
+) => {
+ // Run the callback on the array to get promises
+ const promises = array.map(cb)
+
+ // Use Promise.allSettled to wait for all promises to settle
+ const results = await Promise.allSettled(promises)
+
+ // Extract non-null values from fulfilled promises in a single pass
+ return results.reduce[]>((acc, result) => {
+ if (result.status === 'fulfilled') {
+ const value = result.value
+ if (value) {
+ acc.push(value)
+ }
+ }
+ return acc
+ }, [])
+}