chore: merge stating into use-ndk
This commit is contained in:
commit
4e54d12175
62
package-lock.json
generated
62
package-lock.json
generated
@ -33,8 +33,9 @@
|
|||||||
"idb": "8.0.0",
|
"idb": "8.0.0",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"material-ui-popup-state": "^5.3.1",
|
||||||
"mui-file-input": "4.0.4",
|
"mui-file-input": "4.0.4",
|
||||||
"nostr-login": "^1.6.6",
|
"nostr-login": "1.6.14",
|
||||||
"nostr-tools": "2.7.0",
|
"nostr-tools": "2.7.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^4.4.168",
|
"pdfjs-dist": "^4.4.168",
|
||||||
@ -50,7 +51,8 @@
|
|||||||
"react-toastify": "10.0.4",
|
"react-toastify": "10.0.4",
|
||||||
"redux": "5.0.1",
|
"redux": "5.0.1",
|
||||||
"signature_pad": "^5.0.4",
|
"signature_pad": "^5.0.4",
|
||||||
"tseep": "1.2.1"
|
"tseep": "1.2.1",
|
||||||
|
"use-immer": "^0.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
@ -3379,6 +3381,12 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/classnames": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cli-cursor": {
|
"node_modules/cli-cursor": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||||
@ -3629,10 +3637,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-key": "^3.1.0",
|
"path-key": "^3.1.0",
|
||||||
"shebang-command": "^2.0.0",
|
"shebang-command": "^2.0.0",
|
||||||
@ -6030,6 +6039,26 @@
|
|||||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/material-ui-popup-state": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-mmx1DsQwF/2cmcpHvS/QkUwOQG2oAM+cDEQU0DaZVYnvwKyTB3AFgu8l1/E+LQFausmzpSJoljwQSZXkNvt7eA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.6",
|
||||||
|
"@types/prop-types": "^15.7.3",
|
||||||
|
"@types/react": "^18.0.26",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@mui/material": "^5.0.0 || ^6.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/md5.js": {
|
"node_modules/md5.js": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||||
@ -6281,9 +6310,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -6291,6 +6320,7 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.cjs"
|
||||||
},
|
},
|
||||||
@ -6469,9 +6499,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nostr-login": {
|
"node_modules/nostr-login": {
|
||||||
"version": "1.6.6",
|
"version": "1.6.14",
|
||||||
"resolved": "https://registry.npmjs.org/nostr-login/-/nostr-login-1.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/nostr-login/-/nostr-login-1.6.14.tgz",
|
||||||
"integrity": "sha512-XOpB9nG3Qgt7iea7gA1zn4TaTfUKCKGdCHKwErqLPtMk/q1Rhkzj5cq/66iU0WqC6mSiwENfTy1p4qaM7HzMtg==",
|
"integrity": "sha512-pId1G79kjRW1B9qy6OrA8Not23JSfgmS2VegcKf7Qm9VMC7wYGXg1Ry3FMEAB8p11WoboQ8oJi2TqUGiOf61OQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nostr-dev-kit/ndk": "^2.3.1",
|
"@nostr-dev-kit/ndk": "^2.3.1",
|
||||||
@ -8616,6 +8646,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/use-immer": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-RNAqi3GqsWJ4bcCd4LMBgdzvPmTABam24DUaFiKfX9s3MSorNRz9RDZYJkllJoMHUxVLMDetwAuCDeyWNrp1yA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"immer": ">=8.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
"node_modules/use-sync-external-store": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||||
|
@ -43,8 +43,9 @@
|
|||||||
"idb": "8.0.0",
|
"idb": "8.0.0",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"material-ui-popup-state": "^5.3.1",
|
||||||
"mui-file-input": "4.0.4",
|
"mui-file-input": "4.0.4",
|
||||||
"nostr-login": "^1.6.6",
|
"nostr-login": "1.6.14",
|
||||||
"nostr-tools": "2.7.0",
|
"nostr-tools": "2.7.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^4.4.168",
|
"pdfjs-dist": "^4.4.168",
|
||||||
@ -60,7 +61,8 @@
|
|||||||
"react-toastify": "10.0.4",
|
"react-toastify": "10.0.4",
|
||||||
"redux": "5.0.1",
|
"redux": "5.0.1",
|
||||||
"signature_pad": "^5.0.4",
|
"signature_pad": "^5.0.4",
|
||||||
"tseep": "1.2.1"
|
"tseep": "1.2.1",
|
||||||
|
"use-immer": "^0.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
@ -121,7 +121,17 @@ export const AppBar = () => {
|
|||||||
<Container>
|
<Container>
|
||||||
<Toolbar className={styles.toolbar} disableGutters={true}>
|
<Toolbar className={styles.toolbar} disableGutters={true}>
|
||||||
<Box className={styles.logoWrapper}>
|
<Box className={styles.logoWrapper}>
|
||||||
<img src="/logo.svg" alt="Logo" onClick={() => navigate('/')} />
|
<img
|
||||||
|
src="/logo.svg"
|
||||||
|
alt="Logo"
|
||||||
|
onClick={() => {
|
||||||
|
if (['', '#/'].includes(window.location.hash)) {
|
||||||
|
location.reload()
|
||||||
|
} else {
|
||||||
|
navigate('/')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className={styles.rightSideBox}>
|
<Box className={styles.rightSideBox}>
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { Meta } from '../../types'
|
import { Meta } from '../../types'
|
||||||
import { SigitCardDisplayInfo, SigitStatus, SignStatus } from '../../utils'
|
import {
|
||||||
|
hexToNpub,
|
||||||
|
SigitCardDisplayInfo,
|
||||||
|
SigitStatus,
|
||||||
|
SignStatus
|
||||||
|
} from '../../utils'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { formatTimestamp, npubToHex } from '../../utils'
|
import { formatTimestamp, npubToHex } from '../../utils'
|
||||||
import { appPublicRoutes, appPrivateRoutes } from '../../routes'
|
import { appPublicRoutes, appPrivateRoutes } from '../../routes'
|
||||||
@ -20,6 +25,7 @@ import styles from './style.module.scss'
|
|||||||
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
||||||
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
||||||
import { extractFileExtensions } from '../../utils/file'
|
import { extractFileExtensions } from '../../utils/file'
|
||||||
|
import { useAppSelector } from '../../hooks'
|
||||||
|
|
||||||
type SigitProps = {
|
type SigitProps = {
|
||||||
sigitCreateId: string
|
sigitCreateId: string
|
||||||
@ -32,26 +38,32 @@ export const DisplaySigit = ({
|
|||||||
parsedMeta,
|
parsedMeta,
|
||||||
sigitCreateId: sigitCreateId
|
sigitCreateId: sigitCreateId
|
||||||
}: SigitProps) => {
|
}: SigitProps) => {
|
||||||
|
const { usersPubkey } = useAppSelector((state) => state.auth)
|
||||||
|
|
||||||
const { title, createdAt, submittedBy, signers, signedStatus, isValid } =
|
const { title, createdAt, submittedBy, signers, signedStatus, isValid } =
|
||||||
parsedMeta
|
parsedMeta
|
||||||
|
|
||||||
const { signersStatus, fileHashes } = useSigitMeta(meta)
|
const { signersStatus, fileHashes } = useSigitMeta(meta)
|
||||||
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
|
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
|
||||||
|
|
||||||
|
const currentUserNpub: string = usersPubkey ? hexToNpub(usersPubkey) : ''
|
||||||
|
const currentUserNextSigner =
|
||||||
|
signersStatus[currentUserNpub as `npub1${string}`] === SignStatus.Awaiting
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.itemWrapper}>
|
<div className={styles.itemWrapper}>
|
||||||
{signedStatus === SigitStatus.Complete && (
|
{signedStatus === SigitStatus.Complete || !currentUserNextSigner ? (
|
||||||
<Link
|
<Link
|
||||||
to={`${appPublicRoutes.verify}/${sigitCreateId}`}
|
to={`${appPublicRoutes.verify}/${sigitCreateId}`}
|
||||||
className={styles.insetLink}
|
className={styles.insetLink}
|
||||||
></Link>
|
></Link>
|
||||||
)}
|
) : (
|
||||||
{signedStatus !== SigitStatus.Complete && (
|
|
||||||
<Link
|
<Link
|
||||||
to={`${appPrivateRoutes.sign}/${sigitCreateId}`}
|
to={`${appPrivateRoutes.sign}/${sigitCreateId}`}
|
||||||
className={styles.insetLink}
|
className={styles.insetLink}
|
||||||
></Link>
|
></Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className={`line-clamp-2 ${styles.title}`}>{title}</p>
|
<p className={`line-clamp-2 ${styles.title}`}>{title}</p>
|
||||||
<div className={styles.users}>
|
<div className={styles.users}>
|
||||||
{submittedBy && (
|
{submittedBy && (
|
||||||
|
@ -8,19 +8,25 @@ import {
|
|||||||
Select
|
Select
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { User, UserRole, KeyboardCode } from '../../types'
|
import { User, UserRole, KeyboardCode } from '../../types'
|
||||||
import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
|
import { MouseState, DrawnField, DrawTool } from '../../types/drawing'
|
||||||
import { hexToNpub, npubToHex, getProfileUsername } from '../../utils'
|
import { hexToNpub, npubToHex, getProfileUsername } from '../../utils'
|
||||||
import { SigitFile } from '../../utils/file'
|
import { SigitFile } from '../../utils/file'
|
||||||
import { getToolboxLabelByMarkType } from '../../utils/mark'
|
import { getToolboxLabelByMarkType } from '../../utils/mark'
|
||||||
import { FileDivider } from '../FileDivider'
|
|
||||||
import { ExtensionFileBox } from '../ExtensionFileBox'
|
|
||||||
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf'
|
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf'
|
||||||
import { useScale } from '../../hooks/useScale'
|
import { useScale } from '../../hooks/useScale'
|
||||||
import { AvatarIconButton } from '../UserAvatarIconButton'
|
import { AvatarIconButton } from '../UserAvatarIconButton'
|
||||||
import { UserAvatar } from '../UserAvatar'
|
import { UserAvatar } from '../UserAvatar'
|
||||||
import _ from 'lodash'
|
import { Updater } from 'use-immer'
|
||||||
|
import { FileItem } from './internal/FileItem'
|
||||||
|
import { FileDivider } from '../FileDivider'
|
||||||
|
import { Counterpart } from './internal/Counterpart'
|
||||||
|
|
||||||
|
const MINIMUM_RECT_SIZE = {
|
||||||
|
width: 10,
|
||||||
|
height: 10
|
||||||
|
} as const
|
||||||
import { NDKUserProfile } from '@nostr-dev-kit/ndk'
|
import { NDKUserProfile } from '@nostr-dev-kit/ndk'
|
||||||
|
|
||||||
const DEFAULT_START_SIZE = {
|
const DEFAULT_START_SIZE = {
|
||||||
@ -32,16 +38,25 @@ interface HideSignersForDrawnField {
|
|||||||
[key: number]: boolean
|
[key: number]: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
type PageIndexer = [file: number, page: number]
|
||||||
|
type FieldIndexer = [...PageIndexer, field: number]
|
||||||
|
|
||||||
|
interface DrawPdfFieldsProps {
|
||||||
users: User[]
|
users: User[]
|
||||||
userProfiles: { [key: string]: NDKUserProfile }
|
userProfiles: { [key: string]: NDKUserProfile }
|
||||||
sigitFiles: SigitFile[]
|
sigitFiles: SigitFile[]
|
||||||
setSigitFiles: React.Dispatch<React.SetStateAction<SigitFile[]>>
|
updateSigitFiles: Updater<SigitFile[]>
|
||||||
selectedTool?: DrawTool
|
selectedTool?: DrawTool
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DrawPDFFields = (props: Props) => {
|
export const DrawPDFFields = ({
|
||||||
const { selectedTool, sigitFiles, setSigitFiles, users } = props
|
selectedTool,
|
||||||
|
userProfiles,
|
||||||
|
sigitFiles,
|
||||||
|
updateSigitFiles,
|
||||||
|
users
|
||||||
|
}: DrawPdfFieldsProps) => {
|
||||||
|
const { to, from } = useScale()
|
||||||
|
|
||||||
const signers = users.filter((u) => u.role === UserRole.signer)
|
const signers = users.filter((u) => u.role === UserRole.signer)
|
||||||
const defaultSignerNpub = signers.length ? hexToNpub(signers[0].pubkey) : ''
|
const defaultSignerNpub = signers.length ? hexToNpub(signers[0].pubkey) : ''
|
||||||
@ -54,144 +69,124 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* @param pubkeys
|
* @param pubkeys
|
||||||
* @returns available pubkey or empty string
|
* @returns available pubkey or empty string
|
||||||
*/
|
*/
|
||||||
const getAvailableSigner = (...pubkeys: string[]) => {
|
const getAvailableSigner = useCallback(
|
||||||
const availableSigner: string | undefined = pubkeys.find((pubkey) =>
|
(...pubkeys: string[]) => {
|
||||||
signers.some((s) => s.pubkey === npubToHex(pubkey))
|
const availableSigner: string | undefined = pubkeys.find((pubkey) =>
|
||||||
)
|
signers.some((s) => s.pubkey === npubToHex(pubkey))
|
||||||
return availableSigner || ''
|
)
|
||||||
}
|
return availableSigner || ''
|
||||||
|
},
|
||||||
|
[signers]
|
||||||
|
)
|
||||||
|
|
||||||
const { to, from } = useScale()
|
const [mouseState, setMouseState] = useState<MouseState>({})
|
||||||
|
const [indexer, setIndexer] = useState<PageIndexer | FieldIndexer>()
|
||||||
const [mouseState, setMouseState] = useState<MouseState>({
|
const [field, setField] = useState<
|
||||||
clicked: false
|
DrawnField & {
|
||||||
})
|
x: number
|
||||||
|
y: number
|
||||||
const [activeDrawnField, setActiveDrawnField] = useState<{
|
}
|
||||||
fileIndex: number
|
>()
|
||||||
pageIndex: number
|
const [lastIndexer, setLastIndexer] = useState<FieldIndexer>()
|
||||||
drawnFieldIndex: number
|
|
||||||
}>()
|
|
||||||
const isActiveDrawnField = (
|
const isActiveDrawnField = (
|
||||||
fileIndex: number,
|
fileIndex: number,
|
||||||
pageIndex: number,
|
pageIndex: number,
|
||||||
drawnFieldIndex: number
|
drawnFieldIndex: number
|
||||||
) =>
|
) =>
|
||||||
activeDrawnField?.fileIndex === fileIndex &&
|
lastIndexer &&
|
||||||
activeDrawnField?.pageIndex === pageIndex &&
|
lastIndexer[0] === fileIndex &&
|
||||||
activeDrawnField?.drawnFieldIndex === drawnFieldIndex
|
lastIndexer[1] === pageIndex &&
|
||||||
|
lastIndexer[2] === drawnFieldIndex
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drawing events
|
* 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
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
const getPointerCoordinates = (
|
||||||
window.addEventListener('pointerup', handlePointerUp)
|
event: React.PointerEvent,
|
||||||
window.addEventListener('pointercancel', handlePointerUp)
|
customTarget?: HTMLElement | null
|
||||||
|
) => {
|
||||||
|
const target = customTarget ? customTarget : event.currentTarget
|
||||||
|
const rect = target.getBoundingClientRect()
|
||||||
|
|
||||||
return () => {
|
// Clamp X Y within the target
|
||||||
window.removeEventListener('pointerup', handlePointerUp)
|
const x = Math.max(0, Math.min(event.clientX, rect.right) - rect.left) //x position within the element.
|
||||||
window.removeEventListener('pointercancel', handlePointerUp)
|
const y = Math.max(0, Math.min(event.clientY, rect.bottom) - rect.top) //y position within the element.
|
||||||
|
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
rect
|
||||||
}
|
}
|
||||||
}, [])
|
|
||||||
|
|
||||||
const refreshPdfFiles = () => {
|
|
||||||
setSigitFiles([...sigitFiles])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired only on when left (primary pointer interaction) clicking page image
|
* Fired only on when left (primary pointer interaction) clicking page image
|
||||||
* Creates new drawnElement and pushes in the array
|
* Creates new drawnElement
|
||||||
* It is re rendered and visible right away
|
|
||||||
*
|
*
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param page PdfPage where press happened
|
* @param pageIndexer File and page index
|
||||||
|
* @param pageWidth pdf value used to scale pointer coordinates
|
||||||
*/
|
*/
|
||||||
const handlePointerDown = (
|
const handlePointerDown = useCallback(
|
||||||
event: React.PointerEvent,
|
(
|
||||||
page: PdfPage,
|
event: React.PointerEvent,
|
||||||
fileIndex: number,
|
pageIndexer: PageIndexer,
|
||||||
pageIndex: number
|
pageWidth: number
|
||||||
) => {
|
) => {
|
||||||
// Proceed only if left click
|
// Proceed only if left click
|
||||||
if (event.button !== 0) return
|
if (event.button !== 0) return
|
||||||
|
if (!selectedTool) return
|
||||||
if (!selectedTool) {
|
event.currentTarget.setPointerCapture(event.pointerId)
|
||||||
return
|
const counterpart = getAvailableSigner(lastSigner, defaultSignerNpub)
|
||||||
}
|
|
||||||
|
|
||||||
const { x, y } = getPointerCoordinates(event)
|
|
||||||
|
|
||||||
const newField: DrawnField = {
|
|
||||||
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: getAvailableSigner(lastSigner, defaultSignerNpub),
|
|
||||||
type: selectedTool.identifier
|
|
||||||
}
|
|
||||||
|
|
||||||
page.drawnFields.push(newField)
|
|
||||||
|
|
||||||
setActiveDrawnField({
|
|
||||||
fileIndex,
|
|
||||||
pageIndex,
|
|
||||||
drawnFieldIndex: page.drawnFields.length - 1
|
|
||||||
})
|
|
||||||
setMouseState((prev) => {
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
clicked: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drawing is finished, resets all the variables used to draw
|
|
||||||
* @param event Pointer event
|
|
||||||
*/
|
|
||||||
const handlePointerUp = () => {
|
|
||||||
setMouseState((prev) => {
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
clicked: false,
|
|
||||||
dragging: false,
|
|
||||||
resizing: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 handlePointerMove = (event: React.PointerEvent, page: PdfPage) => {
|
|
||||||
if (mouseState.clicked && selectedTool && event.pointerType === 'mouse') {
|
|
||||||
const lastElementIndex = page.drawnFields.length - 1
|
|
||||||
|
|
||||||
const lastDrawnField = page.drawnFields[lastElementIndex]
|
|
||||||
|
|
||||||
// Return early if we don't have lastDrawnField
|
|
||||||
// Issue noticed in the console when dragging out of bounds
|
|
||||||
// to the page below (without releaseing mouse click)
|
|
||||||
if (!lastDrawnField) return
|
|
||||||
|
|
||||||
const { x, y } = getPointerCoordinates(event)
|
const { x, y } = getPointerCoordinates(event)
|
||||||
|
|
||||||
const width = to(page.width, x) - lastDrawnField.left
|
setIndexer(pageIndexer)
|
||||||
const height = to(page.width, y) - lastDrawnField.top
|
setField({
|
||||||
|
x: to(pageWidth, x),
|
||||||
|
y: to(pageWidth, y),
|
||||||
|
left: to(pageWidth, x),
|
||||||
|
top: to(pageWidth, y),
|
||||||
|
width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width,
|
||||||
|
height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height,
|
||||||
|
type: selectedTool.identifier,
|
||||||
|
counterpart
|
||||||
|
})
|
||||||
|
setMouseState({
|
||||||
|
clicked: true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[defaultSignerNpub, getAvailableSigner, lastSigner, selectedTool, to]
|
||||||
|
)
|
||||||
|
|
||||||
lastDrawnField.width = width
|
/**
|
||||||
lastDrawnField.height = height
|
* After {@link handlePointerDown} creates an drawing element, this function
|
||||||
|
* alters the newly created drawing element, resizing it while pointer moves
|
||||||
|
* @param event Pointer event
|
||||||
|
* @param pageWidth pdf value used to scale pointer coordinates
|
||||||
|
*/
|
||||||
|
const handlePointerMove = useCallback(
|
||||||
|
(event: React.PointerEvent, pageWidth: number) => {
|
||||||
|
if (mouseState.clicked && selectedTool && event.pointerType === 'mouse') {
|
||||||
|
const { x, y } = getPointerCoordinates(event)
|
||||||
|
const pageX = to(pageWidth, x)
|
||||||
|
const pageY = to(pageWidth, y)
|
||||||
|
|
||||||
const currentDrawnFields = page.drawnFields
|
// Calculate left, top, width, and height based on direction
|
||||||
|
setField((prev) => {
|
||||||
|
const left = pageX < prev!.x ? pageX : prev!.x
|
||||||
|
const top = pageY < prev!.y ? pageY : prev!.y
|
||||||
|
|
||||||
currentDrawnFields[lastElementIndex] = lastDrawnField
|
const width = Math.abs(pageX - prev!.x)
|
||||||
|
const height = Math.abs(pageY - prev!.y)
|
||||||
refreshPdfFiles()
|
return { ...prev!, left, top, width, height }
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[mouseState.clicked, selectedTool, to]
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired when event happens on the drawn element which will be moved
|
* Fired when event happens on the drawn element which will be moved
|
||||||
@ -201,22 +196,30 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* y - offsetY
|
* y - offsetY
|
||||||
*
|
*
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param drawnFieldIndex Which we are moving
|
* @param fieldIndexer Which field we are moving
|
||||||
*/
|
*/
|
||||||
const handleDrawnFieldPointerDown = (
|
const handleDrawnFieldPointerDown = (
|
||||||
event: React.PointerEvent,
|
event: React.PointerEvent,
|
||||||
fileIndex: number,
|
fieldIndexer: FieldIndexer
|
||||||
pageIndex: number,
|
|
||||||
drawnFieldIndex: number
|
|
||||||
) => {
|
) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
// Proceed only if left click
|
// Proceed only if left click
|
||||||
if (event.button !== 0) return
|
if (event.button !== 0) return
|
||||||
|
|
||||||
|
event.currentTarget.setPointerCapture(event.pointerId)
|
||||||
const drawingRectangleCoords = getPointerCoordinates(event)
|
const drawingRectangleCoords = getPointerCoordinates(event)
|
||||||
|
const [fileIndex, pageIndex, drawnFieldIndex] = fieldIndexer
|
||||||
|
const page = sigitFiles[fileIndex].pages![pageIndex]
|
||||||
|
const drawnField = page.drawnFields[drawnFieldIndex]
|
||||||
|
|
||||||
setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex })
|
setIndexer(fieldIndexer)
|
||||||
|
setField({
|
||||||
|
...drawnField,
|
||||||
|
x: to(page.width, drawingRectangleCoords.x),
|
||||||
|
y: to(page.width, drawingRectangleCoords.y)
|
||||||
|
})
|
||||||
|
setLastIndexer(fieldIndexer)
|
||||||
setMouseState({
|
setMouseState({
|
||||||
dragging: true,
|
dragging: true,
|
||||||
clicked: false,
|
clicked: false,
|
||||||
@ -226,7 +229,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// make signers dropdown visible
|
// Make signers dropdown visible
|
||||||
setHideSignersForDrawnField((prev) => ({
|
setHideSignersForDrawnField((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[drawnFieldIndex]: false
|
[drawnFieldIndex]: false
|
||||||
@ -236,12 +239,10 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
/**
|
/**
|
||||||
* Moves the drawnElement by the pointer position (pointer can grab anywhere on the drawn element)
|
* Moves the drawnElement by the pointer position (pointer can grab anywhere on the drawn element)
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param drawnField which we are moving
|
* @param pageWidth pdf value used to scale pointer coordinates
|
||||||
* @param pageWidth pdf value which is used to calculate scaled offset
|
|
||||||
*/
|
*/
|
||||||
const handleDrawnFieldPointerMove = (
|
const handleDrawnFieldPointerMove = (
|
||||||
event: React.PointerEvent,
|
event: React.PointerEvent,
|
||||||
drawnField: DrawnField,
|
|
||||||
pageWidth: number
|
pageWidth: number
|
||||||
) => {
|
) => {
|
||||||
if (mouseState.dragging) {
|
if (mouseState.dragging) {
|
||||||
@ -255,18 +256,21 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
let left = to(pageWidth, x - coordsOffset.x)
|
let left = to(pageWidth, x - coordsOffset.x)
|
||||||
let top = to(pageWidth, y - coordsOffset.y)
|
let top = to(pageWidth, y - coordsOffset.y)
|
||||||
|
|
||||||
const rightLimit = to(pageWidth, rect.width) - drawnField.width
|
setField((prev) => {
|
||||||
const bottomLimit = to(pageWidth, rect.height) - drawnField.height
|
const rightLimit = to(pageWidth, rect.width) - prev!.width
|
||||||
|
const bottomLimit = to(pageWidth, rect.height) - prev!.height
|
||||||
|
|
||||||
if (left < 0) left = 0
|
if (left < 0) left = 0
|
||||||
if (top < 0) top = 0
|
if (top < 0) top = 0
|
||||||
if (left > rightLimit) left = rightLimit
|
if (left > rightLimit) left = rightLimit
|
||||||
if (top > bottomLimit) top = bottomLimit
|
if (top > bottomLimit) top = bottomLimit
|
||||||
|
|
||||||
drawnField.left = left
|
return {
|
||||||
drawnField.top = top
|
...prev!,
|
||||||
|
left,
|
||||||
refreshPdfFiles()
|
top
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -274,73 +278,85 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
/**
|
/**
|
||||||
* Fired when clicked on the resize handle, sets the state for a resize action
|
* Fired when clicked on the resize handle, sets the state for a resize action
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param drawnFieldIndex which we are resizing
|
* @param fieldIndexer which field we are resizing
|
||||||
*/
|
*/
|
||||||
const handleResizePointerDown = (
|
const handleResizePointerDown = useCallback(
|
||||||
event: React.PointerEvent,
|
(event: React.PointerEvent, fieldIndexer: FieldIndexer) => {
|
||||||
fileIndex: number,
|
// Proceed only if left click
|
||||||
pageIndex: number,
|
if (event.button !== 0) return
|
||||||
drawnFieldIndex: number
|
event.stopPropagation()
|
||||||
) => {
|
|
||||||
// Proceed only if left click
|
|
||||||
if (event.button !== 0) return
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex })
|
event.currentTarget.setPointerCapture(event.pointerId)
|
||||||
setMouseState({
|
const [fileIndex, pageIndex, drawnFieldIndex] = fieldIndexer
|
||||||
resizing: true
|
const page = sigitFiles[fileIndex].pages![pageIndex]
|
||||||
})
|
const drawnField = page.drawnFields[drawnFieldIndex]
|
||||||
}
|
setIndexer(fieldIndexer)
|
||||||
|
setField({
|
||||||
|
...drawnField,
|
||||||
|
x: drawnField.left,
|
||||||
|
y: drawnField.top
|
||||||
|
})
|
||||||
|
setLastIndexer(fieldIndexer)
|
||||||
|
setMouseState({
|
||||||
|
resizing: true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[sigitFiles]
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resizes the drawn element by the mouse position
|
* Resizes the drawn element by the mouse position
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param drawnField which we are resizing
|
* @param pageWidth pdf value used to scale pointer coordinates
|
||||||
* @param pageWidth pdf value which is used to calculate scaled offset
|
|
||||||
*/
|
*/
|
||||||
const handleResizePointerMove = (
|
const handleResizePointerMove = useCallback(
|
||||||
event: React.PointerEvent,
|
(event: React.PointerEvent, pageWidth: number) => {
|
||||||
drawnField: DrawnField,
|
if (mouseState.resizing) {
|
||||||
pageWidth: number
|
|
||||||
) => {
|
|
||||||
if (mouseState.resizing) {
|
|
||||||
const { x, y } = getPointerCoordinates(
|
|
||||||
event,
|
|
||||||
// currentTarget = span handle
|
// currentTarget = span handle
|
||||||
// 1st parent = drawnField
|
// 1st parent = drawnField
|
||||||
// 2nd parent = img
|
// 2nd parent = img
|
||||||
event.currentTarget.parentElement?.parentElement
|
const { x, y } = getPointerCoordinates(
|
||||||
)
|
event,
|
||||||
|
event.currentTarget.parentElement?.parentElement
|
||||||
|
)
|
||||||
|
|
||||||
const width = to(pageWidth, x) - drawnField.left
|
const pageX = to(pageWidth, x)
|
||||||
const height = to(pageWidth, y) - drawnField.top
|
const pageY = to(pageWidth, y)
|
||||||
|
|
||||||
drawnField.width = width
|
setField((prev) => {
|
||||||
drawnField.height = height
|
const left = pageX < prev!.x ? pageX : prev!.x
|
||||||
|
const top = pageY < prev!.y ? pageY : prev!.y
|
||||||
|
|
||||||
refreshPdfFiles()
|
const width = Math.abs(pageX - prev!.x)
|
||||||
}
|
const height = Math.abs(pageY - prev!.y)
|
||||||
}
|
return { ...prev!, left, top, width, height }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mouseState.resizing, to]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePointerUpReleaseCapture = useCallback(
|
||||||
|
(event: React.PointerEvent) => {
|
||||||
|
event.currentTarget.releasePointerCapture(event.pointerId)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the drawn element using the indexes in the params
|
* Removes the drawn element using the indexes in the params
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param pdfFileIndex pdf file index
|
* @param fieldIIndexer [file index, page index, field index]
|
||||||
* @param pdfPageIndex pdf page index
|
|
||||||
* @param drawnFileIndex drawn file index
|
|
||||||
*/
|
*/
|
||||||
const handleRemovePointerDown = (
|
const handleRemovePointerDown = (
|
||||||
event: React.PointerEvent,
|
event: React.PointerEvent,
|
||||||
pdfFileIndex: number,
|
[fileIndex, pageIndex, fieldIndex]: FieldIndexer
|
||||||
pdfPageIndex: number,
|
|
||||||
drawnFileIndex: number
|
|
||||||
) => {
|
) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
const pages = sigitFiles[pdfFileIndex]?.pages
|
updateSigitFiles((draft) => {
|
||||||
if (pages) {
|
draft[fileIndex]?.pages![pageIndex]?.drawnFields?.splice(fieldIndex, 1)
|
||||||
pages[pdfPageIndex]?.drawnFields?.splice(drawnFileIndex, 1)
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -379,28 +395,72 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the pointer coordinates relative to a element in the `event` param
|
* Drawing is finished, resets all the variables used to draw
|
||||||
* @param event PointerEvent
|
|
||||||
* @param customTarget coordinates relative to this element, if not provided
|
|
||||||
* event.target will be used
|
|
||||||
*/
|
*/
|
||||||
const getPointerCoordinates = (
|
const handlePointerUp = useCallback(() => {
|
||||||
event: React.PointerEvent,
|
// Proceed if we have selected something
|
||||||
customTarget?: HTMLElement | null
|
if (indexer) {
|
||||||
) => {
|
// Check if we have the "preview" field state
|
||||||
const target = customTarget ? customTarget : event.currentTarget
|
if (field) {
|
||||||
const rect = target.getBoundingClientRect()
|
// Cancel update if preview field is below the MINIMUM_RECT_SIZE threshhold
|
||||||
|
if (
|
||||||
|
field.width < MINIMUM_RECT_SIZE.width ||
|
||||||
|
field.height < MINIMUM_RECT_SIZE.height
|
||||||
|
) {
|
||||||
|
setIndexer(undefined)
|
||||||
|
setMouseState({})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Clamp X Y within the target
|
const [fileIndex, pageIndex, fieldIndex] = indexer
|
||||||
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 {
|
// Add new drawn field to the files
|
||||||
x,
|
if (mouseState.clicked) {
|
||||||
y,
|
updateSigitFiles((draft) => {
|
||||||
rect
|
draft[fileIndex].pages![pageIndex].drawnFields.push(field)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move
|
||||||
|
if (mouseState.dragging) {
|
||||||
|
updateSigitFiles((draft) => {
|
||||||
|
draft[fileIndex].pages![pageIndex].drawnFields[fieldIndex!] = field
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
if (mouseState.resizing) {
|
||||||
|
updateSigitFiles((draft) => {
|
||||||
|
draft[fileIndex].pages![pageIndex].drawnFields[fieldIndex!] = field
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear indexer after applying the update
|
||||||
|
setIndexer(undefined)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
setMouseState({})
|
||||||
|
}, [
|
||||||
|
field,
|
||||||
|
indexer,
|
||||||
|
mouseState.clicked,
|
||||||
|
mouseState.dragging,
|
||||||
|
mouseState.resizing,
|
||||||
|
updateSigitFiles
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawing events
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('pointerup', handlePointerUp)
|
||||||
|
window.addEventListener('pointercancel', handlePointerUp)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('pointerup', handlePointerUp)
|
||||||
|
window.removeEventListener('pointercancel', handlePointerUp)
|
||||||
|
}
|
||||||
|
}, [handlePointerUp])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the pdf pages and drawing elements
|
* Renders the pdf pages and drawing elements
|
||||||
@ -412,6 +472,13 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{file.pages?.map((page, pageIndex: number) => {
|
{file.pages?.map((page, pageIndex: number) => {
|
||||||
|
let isPageIndexerActive = false
|
||||||
|
if (indexer) {
|
||||||
|
const [fi, pi, di] = indexer
|
||||||
|
isPageIndexerActive =
|
||||||
|
fi === fileIndex && pi === pageIndex && typeof di === 'undefined'
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={pageIndex}
|
key={pageIndex}
|
||||||
@ -420,32 +487,68 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
onKeyDown={(event) => handleEscapeButtonDown(event)}
|
onKeyDown={(event) => handleEscapeButtonDown(event)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
onPointerMove={(event) => {
|
|
||||||
handlePointerMove(event, page)
|
|
||||||
}}
|
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
handlePointerDown(event, page, fileIndex, pageIndex)
|
handlePointerDown(event, [fileIndex, pageIndex], page.width)
|
||||||
}}
|
}}
|
||||||
|
onPointerMove={(event) => {
|
||||||
|
handlePointerMove(event, page.width)
|
||||||
|
}}
|
||||||
|
onPointerUp={handlePointerUpReleaseCapture}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
src={page.image}
|
src={page.image}
|
||||||
alt={`page ${pageIndex + 1} of ${file.name}`}
|
alt={`page ${pageIndex + 1} of ${file.name}`}
|
||||||
/>
|
/>
|
||||||
|
{isPageIndexerActive && field && (
|
||||||
|
<div
|
||||||
|
className={styles.drawingRectanglePreview}
|
||||||
|
style={{
|
||||||
|
backgroundColor: field.counterpart
|
||||||
|
? `#${npubToHex(field.counterpart)?.substring(0, 6)}4b`
|
||||||
|
: undefined,
|
||||||
|
outlineColor: field.counterpart
|
||||||
|
? `#${npubToHex(field.counterpart)?.substring(0, 6)}`
|
||||||
|
: undefined,
|
||||||
|
left: inPx(from(page.width, field.left)),
|
||||||
|
top: inPx(from(page.width, field.top)),
|
||||||
|
width: inPx(from(page.width, field.width)),
|
||||||
|
height: inPx(from(page.width, field.height))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`file-mark ${styles.placeholder}`}
|
||||||
|
style={{
|
||||||
|
fontFamily: FONT_TYPE,
|
||||||
|
fontSize: inPx(from(page.width, FONT_SIZE))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getToolboxLabelByMarkType(field.type) || 'placeholder'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{page.drawnFields.map((drawnField, drawnFieldIndex: number) => {
|
{page.drawnFields.map((drawnField, drawnFieldIndex: number) => {
|
||||||
|
let isFieldIndexerActive = false
|
||||||
|
if (indexer) {
|
||||||
|
const [fi, pi, di] = indexer
|
||||||
|
isFieldIndexerActive =
|
||||||
|
fi === fileIndex &&
|
||||||
|
pi === pageIndex &&
|
||||||
|
di === drawnFieldIndex
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={drawnFieldIndex}
|
key={drawnFieldIndex}
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
handleDrawnFieldPointerDown(
|
handleDrawnFieldPointerDown(event, [
|
||||||
event,
|
|
||||||
fileIndex,
|
fileIndex,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
drawnFieldIndex
|
drawnFieldIndex
|
||||||
)
|
])
|
||||||
}
|
}
|
||||||
onPointerMove={(event) => {
|
onPointerMove={(event) => {
|
||||||
handleDrawnFieldPointerMove(event, drawnField, page.width)
|
handleDrawnFieldPointerMove(event, page.width)
|
||||||
}}
|
}}
|
||||||
|
onPointerUp={handlePointerUpReleaseCapture}
|
||||||
className={styles.drawingRectangle}
|
className={styles.drawingRectangle}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: drawnField.counterpart
|
backgroundColor: drawnField.counterpart
|
||||||
@ -454,12 +557,29 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
outlineColor: drawnField.counterpart
|
outlineColor: drawnField.counterpart
|
||||||
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}`
|
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}`
|
||||||
: undefined,
|
: undefined,
|
||||||
left: inPx(from(page.width, drawnField.left)),
|
...(isFieldIndexerActive && field
|
||||||
top: inPx(from(page.width, drawnField.top)),
|
? {
|
||||||
width: inPx(from(page.width, drawnField.width)),
|
left: inPx(from(page.width, field.left)),
|
||||||
height: inPx(from(page.width, drawnField.height)),
|
top: inPx(from(page.width, field.top)),
|
||||||
|
width: inPx(from(page.width, field.width)),
|
||||||
|
height: inPx(from(page.width, field.height))
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
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',
|
pointerEvents: mouseState.clicked ? 'none' : 'all',
|
||||||
touchAction: 'none',
|
touchAction: 'none',
|
||||||
|
zIndex: isActiveDrawnField(
|
||||||
|
fileIndex,
|
||||||
|
pageIndex,
|
||||||
|
drawnFieldIndex
|
||||||
|
)
|
||||||
|
? 60
|
||||||
|
: undefined,
|
||||||
opacity:
|
opacity:
|
||||||
mouseState.dragging &&
|
mouseState.dragging &&
|
||||||
isActiveDrawnField(
|
isActiveDrawnField(
|
||||||
@ -483,37 +603,36 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
handleResizePointerDown(
|
handleResizePointerDown(event, [
|
||||||
event,
|
|
||||||
fileIndex,
|
fileIndex,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
drawnFieldIndex
|
drawnFieldIndex
|
||||||
)
|
])
|
||||||
}
|
}
|
||||||
onPointerMove={(event) => {
|
onPointerMove={(event) => {
|
||||||
handleResizePointerMove(event, drawnField, page.width)
|
handleResizePointerMove(event, page.width)
|
||||||
}}
|
}}
|
||||||
|
onPointerUp={handlePointerUpReleaseCapture}
|
||||||
className={styles.resizeHandle}
|
className={styles.resizeHandle}
|
||||||
style={{
|
style={{
|
||||||
background:
|
...(mouseState.resizing &&
|
||||||
mouseState.resizing &&
|
|
||||||
isActiveDrawnField(
|
isActiveDrawnField(
|
||||||
fileIndex,
|
fileIndex,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
drawnFieldIndex
|
drawnFieldIndex
|
||||||
)
|
) && {
|
||||||
? 'var(--primary-main)'
|
cursor: 'grabbing',
|
||||||
: undefined
|
opacity: 0.1
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
></span>
|
></span>
|
||||||
<span
|
<span
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
handleRemovePointerDown(
|
handleRemovePointerDown(event, [
|
||||||
event,
|
|
||||||
fileIndex,
|
fileIndex,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
drawnFieldIndex
|
drawnFieldIndex
|
||||||
)
|
])
|
||||||
}}
|
}}
|
||||||
className={styles.removeHandle}
|
className={styles.removeHandle}
|
||||||
>
|
>
|
||||||
@ -551,21 +670,23 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
drawnField.counterpart = event.target.value
|
drawnField.counterpart = event.target.value
|
||||||
setLastSigner(event.target.value)
|
setLastSigner(event.target.value)
|
||||||
refreshPdfFiles()
|
|
||||||
}}
|
}}
|
||||||
labelId="counterparts"
|
labelId="counterparts"
|
||||||
label="Counterparts"
|
label="Counterparts"
|
||||||
sx={{
|
sx={{
|
||||||
background: 'white'
|
background: 'white'
|
||||||
}}
|
}}
|
||||||
renderValue={(value) =>
|
renderValue={(value) => (
|
||||||
renderCounterpartValue(value)
|
<Counterpart
|
||||||
}
|
npub={value}
|
||||||
|
metadata={userProfiles}
|
||||||
|
signers={signers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{signers.map((signer, index) => {
|
{signers.map((signer, index) => {
|
||||||
const npub = hexToNpub(signer.pubkey)
|
const npub = hexToNpub(signer.pubkey)
|
||||||
const profile =
|
const profile = userProfiles[signer.pubkey]
|
||||||
props.userProfiles[signer.pubkey]
|
|
||||||
const displayValue = getProfileUsername(
|
const displayValue = getProfileUsername(
|
||||||
npub,
|
npub,
|
||||||
profile
|
profile
|
||||||
@ -618,58 +739,24 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderCounterpartValue = (npub: string) => {
|
|
||||||
let displayValue = _.truncate(npub, { length: 16 })
|
|
||||||
|
|
||||||
const signer = signers.find((u) => u.pubkey === npubToHex(npub))
|
|
||||||
if (signer) {
|
|
||||||
const profile = props.userProfiles[signer.pubkey]
|
|
||||||
displayValue = getProfileUsername(npub, profile)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.counterpartSelectValue}>
|
|
||||||
<AvatarIconButton
|
|
||||||
src={profile?.image}
|
|
||||||
hexKey={signer.pubkey || undefined}
|
|
||||||
sx={{
|
|
||||||
padding: 0,
|
|
||||||
marginRight: '6px',
|
|
||||||
'> img': {
|
|
||||||
width: '21px',
|
|
||||||
height: '21px'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{displayValue}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return displayValue
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="files-wrapper">
|
<div className="files-wrapper">
|
||||||
{sigitFiles.map((file, i) => {
|
{sigitFiles.length > 0 &&
|
||||||
return (
|
sigitFiles
|
||||||
<React.Fragment key={file.name}>
|
.map<React.ReactNode>((file, i) =>
|
||||||
<div className="file-wrapper" id={`file-${file.name}`}>
|
file.isPdf ? (
|
||||||
{file.isPdf && getPdfPages(file, i)}
|
<React.Fragment key={file.name}>
|
||||||
{file.isImage && (
|
{getPdfPages(file, i)}
|
||||||
<img
|
</React.Fragment>
|
||||||
className="file-image"
|
) : (
|
||||||
src={file.objectUrl}
|
<FileItem key={file.name} file={file} />
|
||||||
alt={file.name}
|
)
|
||||||
/>
|
)
|
||||||
)}
|
.reduce((prev, curr, i) => [
|
||||||
{!(file.isPdf || file.isImage) && (
|
prev,
|
||||||
<ExtensionFileBox extension={file.extension} />
|
<FileDivider key={`separator-${i}`} />,
|
||||||
)}
|
curr
|
||||||
</div>
|
])}
|
||||||
{i < sigitFiles.length - 1 && <FileDivider />}
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.counterpartSelectValue {
|
||||||
|
display: flex;
|
||||||
|
}
|
46
src/components/DrawPDFFields/internal/Counterpart.tsx
Normal file
46
src/components/DrawPDFFields/internal/Counterpart.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { ProfileMetadata, User } from '../../../types'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { npubToHex, getProfileUsername } from '../../../utils'
|
||||||
|
import { AvatarIconButton } from '../../UserAvatarIconButton'
|
||||||
|
import styles from './Counterpart.module.scss'
|
||||||
|
|
||||||
|
interface CounterpartProps {
|
||||||
|
npub: string
|
||||||
|
metadata: {
|
||||||
|
[key: string]: ProfileMetadata
|
||||||
|
}
|
||||||
|
signers: User[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Counterpart = React.memo(
|
||||||
|
({ npub, metadata, signers }: CounterpartProps) => {
|
||||||
|
let displayValue = _.truncate(npub, { length: 16 })
|
||||||
|
|
||||||
|
const signer = signers.find((u) => u.pubkey === npubToHex(npub))
|
||||||
|
if (signer) {
|
||||||
|
const signerMetadata = metadata[signer.pubkey]
|
||||||
|
displayValue = getProfileUsername(npub, signerMetadata)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.counterpartSelectValue}>
|
||||||
|
<AvatarIconButton
|
||||||
|
src={signerMetadata.picture}
|
||||||
|
hexKey={signer.pubkey || undefined}
|
||||||
|
sx={{
|
||||||
|
padding: 0,
|
||||||
|
marginRight: '6px',
|
||||||
|
'> img': {
|
||||||
|
width: '21px',
|
||||||
|
height: '21px'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{displayValue}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayValue
|
||||||
|
}
|
||||||
|
)
|
19
src/components/DrawPDFFields/internal/FileItem.tsx
Normal file
19
src/components/DrawPDFFields/internal/FileItem.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { SigitFile } from '../../../utils/file'
|
||||||
|
import { ExtensionFileBox } from '../../ExtensionFileBox'
|
||||||
|
import { ImageItem } from './ImageItem'
|
||||||
|
|
||||||
|
interface FileItemProps {
|
||||||
|
file: SigitFile
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileItem = React.memo(({ file }: FileItemProps) => {
|
||||||
|
const content = <ExtensionFileBox extension={file.extension} />
|
||||||
|
if (file.isImage) return <ImageItem file={file} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={file.name} className="file-wrapper" id={`file-${file.name}`}>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
10
src/components/DrawPDFFields/internal/ImageItem.tsx
Normal file
10
src/components/DrawPDFFields/internal/ImageItem.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { SigitFile } from '../../../utils/file'
|
||||||
|
|
||||||
|
interface ImageItemProps {
|
||||||
|
file: SigitFile
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImageItem = React.memo(({ file }: ImageItemProps) => {
|
||||||
|
return <img className="file-image" src={file.objectUrl} alt={file.name} />
|
||||||
|
})
|
@ -38,10 +38,6 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.edited {
|
|
||||||
outline: 1px dotted #01aaad;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resizeHandle {
|
.resizeHandle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -5px;
|
right: -5px;
|
||||||
@ -51,7 +47,7 @@
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 1px solid rgb(160, 160, 160);
|
border: 1px solid rgb(160, 160, 160);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: nwse-resize;
|
cursor: grab;
|
||||||
|
|
||||||
// Increase the area a bit so it's easier to click
|
// Increase the area a bit so it's easier to click
|
||||||
&::after {
|
&::after {
|
||||||
@ -89,13 +85,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.counterpartSelectValue {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.counterpartAvatar {
|
.counterpartAvatar {
|
||||||
img {
|
img {
|
||||||
width: 21px;
|
width: 21px;
|
||||||
height: 21px;
|
height: 21px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signingRectangle {
|
||||||
|
position: absolute;
|
||||||
|
outline: 1px solid #01aaad;
|
||||||
|
z-index: 40;
|
||||||
|
background-color: #01aaad4b;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.edited {
|
||||||
|
outline: 1px dotted #01aaad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawingRectanglePreview {
|
||||||
|
position: absolute;
|
||||||
|
outline: 1px solid;
|
||||||
|
z-index: 50;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: none;
|
||||||
|
touch-action: none;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
import { CurrentUserFile } from '../../types/file.ts'
|
import { CurrentUserFile } from '../../types/file.ts'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { Button } from '@mui/material'
|
import { Button, Menu, MenuItem } from '@mui/material'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
interface FileListProps {
|
interface FileListProps {
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
currentFile: CurrentUserFile
|
currentFile: CurrentUserFile
|
||||||
setCurrentFile: (file: CurrentUserFile) => void
|
setCurrentFile: (file: CurrentUserFile) => void
|
||||||
handleDownload: () => void
|
handleExport: () => void
|
||||||
downloadLabel?: string
|
handleEncryptedExport?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileList = ({
|
const FileList = ({
|
||||||
files,
|
files,
|
||||||
currentFile,
|
currentFile,
|
||||||
setCurrentFile,
|
setCurrentFile,
|
||||||
handleDownload,
|
handleExport,
|
||||||
downloadLabel
|
handleEncryptedExport
|
||||||
}: FileListProps) => {
|
}: FileListProps) => {
|
||||||
const isActive = (file: CurrentUserFile) => file.id === currentFile.id
|
const isActive = (file: CurrentUserFile) => file.id === currentFile.id
|
||||||
return (
|
return (
|
||||||
@ -42,9 +44,35 @@ const FileList = ({
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<Button variant="contained" fullWidth onClick={handleDownload}>
|
|
||||||
{downloadLabel || 'Download Files'}
|
<PopupState variant="popover" popupId="download-popup-menu">
|
||||||
</Button>
|
{(popupState) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button variant="contained" {...bindTrigger(popupState)}>
|
||||||
|
Export files
|
||||||
|
</Button>
|
||||||
|
<Menu {...bindMenu(popupState)}>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
popupState.close
|
||||||
|
handleExport()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Export Files
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
popupState.close
|
||||||
|
typeof handleEncryptedExport === 'function' &&
|
||||||
|
handleEncryptedExport()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Export Encrypted Files
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</PopupState>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,12 @@ export const Footer = () =>
|
|||||||
}}
|
}}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={'/'}
|
to={'/'}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (['', '#/'].includes(window.location.hash)) {
|
||||||
|
event.preventDefault()
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}
|
||||||
|
}}
|
||||||
variant={'text'}
|
variant={'text'}
|
||||||
>
|
>
|
||||||
Home
|
Home
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
z-index: 50;
|
z-index: 70;
|
||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(10px);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { CurrentUserMark } from '../../types/mark.ts'
|
import { CurrentUserMark } from '../../types/mark.ts'
|
||||||
import styles from './style.module.scss'
|
|
||||||
import {
|
import {
|
||||||
findNextIncompleteCurrentUserMark,
|
findNextIncompleteCurrentUserMark,
|
||||||
getToolboxLabelByMarkType,
|
getToolboxLabelByMarkType,
|
||||||
@ -8,12 +7,16 @@ import {
|
|||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
|
import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { Button } from '@mui/material'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
|
||||||
interface MarkFormFieldProps {
|
interface MarkFormFieldProps {
|
||||||
currentUserMarks: CurrentUserMark[]
|
currentUserMarks: CurrentUserMark[]
|
||||||
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
|
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
|
||||||
handleSelectedMarkValueChange: (value: string) => void
|
handleSelectedMarkValueChange: (value: string) => void
|
||||||
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
handleSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
selectedMark: CurrentUserMark
|
selectedMark: CurrentUserMark
|
||||||
selectedMarkValue: string
|
selectedMarkValue: string
|
||||||
}
|
}
|
||||||
@ -30,11 +33,13 @@ const MarkFormField = ({
|
|||||||
handleCurrentUserMarkChange
|
handleCurrentUserMarkChange
|
||||||
}: MarkFormFieldProps) => {
|
}: MarkFormFieldProps) => {
|
||||||
const [displayActions, setDisplayActions] = useState(true)
|
const [displayActions, setDisplayActions] = useState(true)
|
||||||
|
const [complete, setComplete] = useState(false)
|
||||||
|
|
||||||
const isReadyToSign = () =>
|
const isReadyToSign = () =>
|
||||||
isCurrentUserMarksComplete(currentUserMarks) ||
|
isCurrentUserMarksComplete(currentUserMarks) ||
|
||||||
isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue)
|
isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue)
|
||||||
const isCurrent = (currentMark: CurrentUserMark) =>
|
const isCurrent = (currentMark: CurrentUserMark) =>
|
||||||
currentMark.id === selectedMark.id
|
currentMark.id === selectedMark.id && !complete
|
||||||
const isDone = (currentMark: CurrentUserMark) =>
|
const isDone = (currentMark: CurrentUserMark) =>
|
||||||
isCurrent(currentMark) ? !!selectedMarkValue : currentMark.isCompleted
|
isCurrent(currentMark) ? !!selectedMarkValue : currentMark.isCompleted
|
||||||
const findNext = () => {
|
const findNext = () => {
|
||||||
@ -46,13 +51,36 @@ const MarkFormField = ({
|
|||||||
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
console.log('handle form submit runs...')
|
console.log('handle form submit runs...')
|
||||||
return isReadyToSign()
|
|
||||||
? handleSubmit(event)
|
// Without this line, we lose mark values when switching
|
||||||
: handleCurrentUserMarkChange(findNext()!)
|
handleCurrentUserMarkChange(selectedMark)
|
||||||
|
|
||||||
|
if (!complete) {
|
||||||
|
isReadyToSign()
|
||||||
|
? setComplete(true)
|
||||||
|
: handleCurrentUserMarkChange(findNext()!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleActions = () => setDisplayActions(!displayActions)
|
const toggleActions = () => setDisplayActions(!displayActions)
|
||||||
const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type)
|
const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type)
|
||||||
|
|
||||||
|
const handleCurrentUserMarkClick = (mark: CurrentUserMark) => {
|
||||||
|
setComplete(false)
|
||||||
|
handleCurrentUserMarkChange(mark)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectCompleteMark = () => {
|
||||||
|
handleCurrentUserMarkChange(selectedMark)
|
||||||
|
setComplete(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSignAndComplete = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
|
) => {
|
||||||
|
handleSubmit(event)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.trigger}>
|
<div className={styles.trigger}>
|
||||||
@ -78,33 +106,55 @@ const MarkFormField = ({
|
|||||||
<div className={styles.actionsWrapper}>
|
<div className={styles.actionsWrapper}>
|
||||||
<div className={styles.actionsTop}>
|
<div className={styles.actionsTop}>
|
||||||
<div className={styles.actionsTopInfo}>
|
<div className={styles.actionsTopInfo}>
|
||||||
<p className={styles.actionsTopInfoText}>Add {markLabel}</p>
|
{!complete && (
|
||||||
|
<p className={styles.actionsTopInfoText}>Add {markLabel}</p>
|
||||||
|
)}
|
||||||
|
{complete && <p className={styles.actionsTopInfoText}>Finish</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.inputWrapper}>
|
<div className={styles.inputWrapper}>
|
||||||
<form onSubmit={(e) => handleFormSubmit(e)}>
|
{!complete && (
|
||||||
<MarkInput
|
<form onSubmit={(e) => handleFormSubmit(e)}>
|
||||||
markType={selectedMark.mark.type}
|
<MarkInput
|
||||||
key={selectedMark.id}
|
markType={selectedMark.mark.type}
|
||||||
value={selectedMarkValue}
|
key={selectedMark.id}
|
||||||
placeholder={markLabel}
|
value={selectedMarkValue}
|
||||||
handler={handleSelectedMarkValueChange}
|
placeholder={markLabel}
|
||||||
userMark={selectedMark}
|
handler={handleSelectedMarkValueChange}
|
||||||
/>
|
userMark={selectedMark}
|
||||||
|
/>
|
||||||
|
<div className={styles.actionsBottom}>
|
||||||
|
<Button type="submit" className={styles.submitButton}>
|
||||||
|
NEXT
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{complete && (
|
||||||
<div className={styles.actionsBottom}>
|
<div className={styles.actionsBottom}>
|
||||||
<button type="submit" className={styles.submitButton}>
|
<Button
|
||||||
NEXT
|
onClick={handleSignAndComplete}
|
||||||
</button>
|
className={[styles.submitButton, styles.completeButton].join(
|
||||||
|
' '
|
||||||
|
)}
|
||||||
|
disabled={!isReadyToSign()}
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
SIGN AND COMPLETE
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
)}
|
||||||
|
|
||||||
<div className={styles.footerContainer}>
|
<div className={styles.footerContainer}>
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
{currentUserMarks.map((mark, index) => {
|
{currentUserMarks.map((mark, index) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.pagination} key={index}>
|
<div className={styles.pagination} key={index}>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={`${styles.paginationButton} ${isDone(mark) ? styles.paginationButtonDone : ''}`}
|
className={`${styles.paginationButton} ${isDone(mark) ? styles.paginationButtonDone : ''}`}
|
||||||
onClick={() => handleCurrentUserMarkChange(mark)}
|
onClick={() => handleCurrentUserMarkClick(mark)}
|
||||||
>
|
>
|
||||||
{mark.id}
|
{mark.id}
|
||||||
</button>
|
</button>
|
||||||
@ -114,6 +164,22 @@ const MarkFormField = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
<div className={styles.pagination}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${styles.paginationButton} ${isReadyToSign() ? styles.paginationButtonDone : ''}`}
|
||||||
|
onClick={handleSelectCompleteMark}
|
||||||
|
title="Complete"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className={styles.finishPage}
|
||||||
|
icon={faCheck}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{complete && (
|
||||||
|
<div className={styles.paginationButtonCurrent}></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,6 +70,11 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.completeButton {
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.paginationButton {
|
.paginationButton {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
@ -78,7 +83,8 @@
|
|||||||
color: rgba(0, 0, 0, 0.5);
|
color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationButton:hover {
|
.paginationButton:hover,
|
||||||
|
.paginationButton:focus {
|
||||||
background: #447592;
|
background: #447592;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
@ -122,7 +128,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
grid-gap: 15px;
|
grid-gap: 15px;
|
||||||
box-shadow: 0 -2px 4px 0 rgb(0, 0, 0, 0.1);
|
box-shadow: 0 -2px 4px 0 rgb(0, 0, 0, 0.1);
|
||||||
max-width: 750px;
|
max-width: 450px;
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -216,3 +222,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
grid-gap: 5px;
|
grid-gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.finishPage {
|
||||||
|
padding: 1px 0;
|
||||||
|
}
|
||||||
|
@ -32,7 +32,7 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={`file-mark ${styles.drawingRectangle} ${isEdited() && styles.edited}`}
|
className={`file-mark ${styles.signingRectangle} ${isEdited() && styles.edited}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: selectedMark?.mark.npub
|
backgroundColor: selectedMark?.mark.npub
|
||||||
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}4b`
|
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}4b`
|
||||||
|
@ -24,11 +24,12 @@ import {
|
|||||||
interface PdfMarkingProps {
|
interface PdfMarkingProps {
|
||||||
currentUserMarks: CurrentUserMark[]
|
currentUserMarks: CurrentUserMark[]
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
handleDownload: () => void
|
handleExport: () => void
|
||||||
|
handleEncryptedExport: () => void
|
||||||
|
handleSign: () => void
|
||||||
meta: Meta | null
|
meta: Meta | null
|
||||||
otherUserMarks: Mark[]
|
otherUserMarks: Mark[]
|
||||||
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
|
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
|
||||||
setIsMarksCompleted: (isMarksCompleted: boolean) => void
|
|
||||||
setUpdatedMarks: (markToUpdate: Mark) => void
|
setUpdatedMarks: (markToUpdate: Mark) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,10 +43,11 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
const {
|
const {
|
||||||
files,
|
files,
|
||||||
currentUserMarks,
|
currentUserMarks,
|
||||||
setIsMarksCompleted,
|
|
||||||
setCurrentUserMarks,
|
setCurrentUserMarks,
|
||||||
setUpdatedMarks,
|
setUpdatedMarks,
|
||||||
handleDownload,
|
handleExport,
|
||||||
|
handleEncryptedExport,
|
||||||
|
handleSign,
|
||||||
meta,
|
meta,
|
||||||
otherUserMarks
|
otherUserMarks
|
||||||
} = props
|
} = props
|
||||||
@ -70,8 +72,8 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
|
|
||||||
const handleMarkClick = (id: number) => {
|
const handleMarkClick = (id: number) => {
|
||||||
const nextMark = currentUserMarks.find((mark) => mark.mark.id === id)
|
const nextMark = currentUserMarks.find((mark) => mark.mark.id === id)
|
||||||
setSelectedMark(nextMark!)
|
|
||||||
setSelectedMarkValue(nextMark?.mark.value ?? EMPTY)
|
if (nextMark) handleCurrentUserMarkChange(nextMark)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCurrentUserMarkChange = (mark: CurrentUserMark) => {
|
const handleCurrentUserMarkChange = (mark: CurrentUserMark) => {
|
||||||
@ -86,11 +88,18 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
updatedSelectedMark
|
updatedSelectedMark
|
||||||
)
|
)
|
||||||
setCurrentUserMarks(updatedCurrentUserMarks)
|
setCurrentUserMarks(updatedCurrentUserMarks)
|
||||||
setSelectedMarkValue(mark.currentValue ?? EMPTY)
|
|
||||||
setSelectedMark(mark)
|
// If clicking on the same mark, don't update the value, otherwise do update
|
||||||
|
if (mark.id !== selectedMark.id) {
|
||||||
|
setSelectedMarkValue(mark.currentValue ?? EMPTY)
|
||||||
|
setSelectedMark(mark)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
/**
|
||||||
|
* Sign and Complete
|
||||||
|
*/
|
||||||
|
const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (!selectedMarkValue || !selectedMark) return
|
if (!selectedMarkValue || !selectedMark) return
|
||||||
|
|
||||||
@ -106,8 +115,8 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
)
|
)
|
||||||
setCurrentUserMarks(updatedCurrentUserMarks)
|
setCurrentUserMarks(updatedCurrentUserMarks)
|
||||||
setSelectedMark(null)
|
setSelectedMark(null)
|
||||||
setIsMarksCompleted(true)
|
|
||||||
setUpdatedMarks(updatedMark.mark)
|
setUpdatedMarks(updatedMark.mark)
|
||||||
|
handleSign()
|
||||||
}
|
}
|
||||||
|
|
||||||
// const updateCurrentUserMarkValues = () => {
|
// const updateCurrentUserMarkValues = () => {
|
||||||
@ -132,7 +141,8 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
files={files}
|
files={files}
|
||||||
currentFile={currentFile}
|
currentFile={currentFile}
|
||||||
setCurrentFile={setCurrentFile}
|
setCurrentFile={setCurrentFile}
|
||||||
handleDownload={handleDownload}
|
handleExport={handleExport}
|
||||||
|
handleEncryptedExport={handleEncryptedExport}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,18 +45,17 @@ const PdfView = ({
|
|||||||
const filterMarksByFile = (marks: Mark[], hash: string): Mark[] => {
|
const filterMarksByFile = (marks: Mark[], hash: string): Mark[] => {
|
||||||
return marks.filter((mark) => mark.pdfFileHash === hash)
|
return marks.filter((mark) => mark.pdfFileHash === hash)
|
||||||
}
|
}
|
||||||
const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean =>
|
|
||||||
index !== files.length - 1
|
|
||||||
return (
|
return (
|
||||||
<div className="files-wrapper">
|
<div className="files-wrapper">
|
||||||
{files.length > 0 ? (
|
{files.length > 0 ? (
|
||||||
files.map((currentUserFile, index, arr) => {
|
files
|
||||||
const { hash, file, id } = currentUserFile
|
.map<React.ReactNode>((currentUserFile) => {
|
||||||
|
const { hash, file, id } = currentUserFile
|
||||||
|
|
||||||
if (!hash) return
|
if (!hash) return
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
|
||||||
<div
|
<div
|
||||||
|
key={`file-${file.name}`}
|
||||||
id={file.name}
|
id={file.name}
|
||||||
className="file-wrapper"
|
className="file-wrapper"
|
||||||
ref={(el) => (pdfRefs.current[id] = el)}
|
ref={(el) => (pdfRefs.current[id] = el)}
|
||||||
@ -70,10 +69,13 @@ const PdfView = ({
|
|||||||
otherUserMarks={filterMarksByFile(otherUserMarks, hash)}
|
otherUserMarks={filterMarksByFile(otherUserMarks, hash)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isNotLastPdfFile(index, arr) && <FileDivider />}
|
)
|
||||||
</React.Fragment>
|
})
|
||||||
)
|
.reduce((prev, curr, i) => [
|
||||||
})
|
prev,
|
||||||
|
<FileDivider key={`separator-${i}`} />,
|
||||||
|
curr
|
||||||
|
])
|
||||||
) : (
|
) : (
|
||||||
<LoadingSpinner variant="small" />
|
<LoadingSpinner variant="small" />
|
||||||
)}
|
)}
|
||||||
|
@ -19,11 +19,18 @@ import {
|
|||||||
updateUserAppData as updateUserAppDataAction
|
updateUserAppData as updateUserAppDataAction
|
||||||
} from '../store/actions'
|
} from '../store/actions'
|
||||||
import { Keys } from '../store/auth/types'
|
import { Keys } from '../store/auth/types'
|
||||||
import { Meta, UserAppData, UserRelaysType } from '../types'
|
import {
|
||||||
|
isSigitNotification,
|
||||||
|
Meta,
|
||||||
|
SigitNotification,
|
||||||
|
UserAppData,
|
||||||
|
UserRelaysType
|
||||||
|
} from '../types'
|
||||||
import {
|
import {
|
||||||
countLeadingZeroes,
|
countLeadingZeroes,
|
||||||
createWrap,
|
createWrap,
|
||||||
deleteBlossomFile,
|
deleteBlossomFile,
|
||||||
|
fetchMetaFromFileStorage,
|
||||||
getDTagForUserAppData,
|
getDTagForUserAppData,
|
||||||
getUserAppDataFromBlossom,
|
getUserAppDataFromBlossom,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
@ -337,21 +344,63 @@ export const useNDK = () => {
|
|||||||
|
|
||||||
if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938) return
|
if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938) return
|
||||||
|
|
||||||
const meta = await parseJson<Meta>(internalUnsignedEvent.content).catch(
|
const parsedContent = await parseJson<Meta | SigitNotification>(
|
||||||
(err) => {
|
internalUnsignedEvent.content
|
||||||
console.log(
|
).catch((err) => {
|
||||||
'An error occurred in parsing the internal unsigned event',
|
console.log(
|
||||||
err
|
'An error occurred in parsing the internal unsigned event',
|
||||||
)
|
err
|
||||||
return null
|
)
|
||||||
}
|
return null
|
||||||
)
|
})
|
||||||
|
|
||||||
if (!meta) return
|
if (!parsedContent) return
|
||||||
|
|
||||||
|
let meta: Meta
|
||||||
|
|
||||||
|
if (isSigitNotification(parsedContent)) {
|
||||||
|
const notification = parsedContent
|
||||||
|
if (!notification.keys || !usersPubkey) return
|
||||||
|
|
||||||
|
let encryptionKey: string | undefined
|
||||||
|
|
||||||
|
const { sender, keys } = notification.keys
|
||||||
|
|
||||||
|
// Retrieve the user's public key from the state
|
||||||
|
const usersNpub = hexToNpub(usersPubkey)
|
||||||
|
|
||||||
|
// Check if the user's public key is in the keys object
|
||||||
|
if (usersNpub in keys) {
|
||||||
|
// Instantiate the NostrController to decrypt the encryption key
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
const decrypted = await nostrController
|
||||||
|
.nip04Decrypt(sender, keys[usersNpub])
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('An error occurred in decrypting encryption key', err)
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
encryptionKey = decrypted
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
meta = await fetchMetaFromFileStorage(
|
||||||
|
notification.metaUrl,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`An error occured fetching meta file from storage`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
meta = parsedContent
|
||||||
|
}
|
||||||
|
|
||||||
await updateUsersAppData(meta)
|
await updateUsersAppData(meta)
|
||||||
},
|
},
|
||||||
[dispatch, processedEvents, updateUsersAppData]
|
[dispatch, processedEvents, updateUsersAppData, usersPubkey]
|
||||||
)
|
)
|
||||||
|
|
||||||
const subscribeForSigits = useCallback(
|
const subscribeForSigits = useCallback(
|
||||||
@ -376,15 +425,20 @@ export const useNDK = () => {
|
|||||||
[fetchEventsFromUserRelays, processReceivedEvent]
|
[fetchEventsFromUserRelays, processReceivedEvent]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to send a notification to a specified receiver.
|
||||||
|
* @param receiver - The recipient's public key.
|
||||||
|
* @param notification - Url pointing to metadata associated with the notification on blossom and keys to decrypt.
|
||||||
|
*/
|
||||||
const sendNotification = useCallback(
|
const sendNotification = useCallback(
|
||||||
async (receiver: string, meta: Meta) => {
|
async (receiver: string, notification: SigitNotification) => {
|
||||||
if (!usersPubkey) return
|
if (!usersPubkey) return
|
||||||
|
|
||||||
// Create an unsigned event object with the provided metadata
|
// Create an unsigned event object with the provided metadata
|
||||||
const unsignedEvent: UnsignedEvent = {
|
const unsignedEvent: UnsignedEvent = {
|
||||||
kind: 938,
|
kind: 938,
|
||||||
pubkey: usersPubkey,
|
pubkey: usersPubkey,
|
||||||
content: JSON.stringify(meta),
|
content: JSON.stringify(notification),
|
||||||
tags: [],
|
tags: [],
|
||||||
created_at: unixNow()
|
created_at: unixNow()
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ export interface FlatMeta
|
|||||||
isValid: boolean
|
isValid: boolean
|
||||||
|
|
||||||
// Decryption
|
// Decryption
|
||||||
encryptionKey: string | null
|
encryptionKey: string | undefined
|
||||||
|
|
||||||
// Parsed Document Signatures
|
// Parsed Document Signatures
|
||||||
parsedSignatureEvents: {
|
parsedSignatureEvents: {
|
||||||
@ -101,7 +101,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
[signer: `npub1${string}`]: SignStatus
|
[signer: `npub1${string}`]: SignStatus
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
const [encryptionKey, setEncryptionKey] = useState<string | null>(null)
|
const [encryptionKey, setEncryptionKey] = useState<string | undefined>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!meta) return
|
if (!meta) return
|
||||||
@ -143,7 +143,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
setMarkConfig(markConfig)
|
setMarkConfig(markConfig)
|
||||||
setZipUrl(zipUrl)
|
setZipUrl(zipUrl)
|
||||||
|
|
||||||
let encryptionKey: string | null = null
|
let encryptionKey: string | undefined
|
||||||
if (meta.keys) {
|
if (meta.keys) {
|
||||||
const { sender, keys } = meta.keys
|
const { sender, keys } = meta.keys
|
||||||
// Retrieve the user's public key from the state
|
// Retrieve the user's public key from the state
|
||||||
@ -161,7 +161,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
'An error occurred in decrypting encryption key',
|
'An error occurred in decrypting encryption key',
|
||||||
err
|
err
|
||||||
)
|
)
|
||||||
return null
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
encryptionKey = decrypted
|
encryptionKey = decrypted
|
||||||
|
@ -138,7 +138,15 @@ export const MainLayout = () => {
|
|||||||
initNostrLogin({
|
initNostrLogin({
|
||||||
methods: ['connect', 'extension', 'local'],
|
methods: ['connect', 'extension', 'local'],
|
||||||
noBanner: true,
|
noBanner: true,
|
||||||
onAuth: handleNostrAuth
|
onAuth: handleNostrAuth,
|
||||||
|
outboxRelays: [
|
||||||
|
'wss://purplepag.es',
|
||||||
|
'wss://relay.nos.social',
|
||||||
|
'wss://user.kindpag.es',
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://nos.lol',
|
||||||
|
'wss://relay.sigit.io'
|
||||||
|
]
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('Failed to initialize Nostr-Login', error)
|
console.error('Failed to initialize Nostr-Login', error)
|
||||||
})
|
})
|
||||||
|
@ -20,11 +20,12 @@ import { toast } from 'react-toastify'
|
|||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { UserAvatar } from '../../components/UserAvatar'
|
import { UserAvatar } from '../../components/UserAvatar'
|
||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { appPrivateRoutes } from '../../routes'
|
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
||||||
import {
|
import {
|
||||||
CreateSignatureEventContent,
|
CreateSignatureEventContent,
|
||||||
KeyboardCode,
|
KeyboardCode,
|
||||||
Meta,
|
Meta,
|
||||||
|
SigitNotification,
|
||||||
SignedEvent,
|
SignedEvent,
|
||||||
User,
|
User,
|
||||||
UserRelaysType,
|
UserRelaysType,
|
||||||
@ -45,7 +46,8 @@ import {
|
|||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
uploadToFileStorage,
|
uploadToFileStorage,
|
||||||
DEFAULT_TOOLBOX,
|
DEFAULT_TOOLBOX,
|
||||||
settleAllFullfilfedPromises
|
settleAllFullfilfedPromises,
|
||||||
|
uploadMetaToFileStorage
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import fileListStyles from '../../components/FileList/style.module.scss'
|
import fileListStyles from '../../components/FileList/style.module.scss'
|
||||||
@ -69,13 +71,14 @@ import {
|
|||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { getSigitFile, SigitFile } from '../../utils/file.ts'
|
import { getSigitFile, SigitFile } from '../../utils/file.ts'
|
||||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
import { Autocomplete } from '@mui/lab'
|
import { Autocomplete } from '@mui/material'
|
||||||
import _, { truncate } from 'lodash'
|
import _, { truncate } from 'lodash'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { AvatarIconButton } from '../../components/UserAvatarIconButton'
|
import { AvatarIconButton } from '../../components/UserAvatarIconButton'
|
||||||
import { NDKUserProfile, NostrEvent } from '@nostr-dev-kit/ndk'
|
import { NDKUserProfile, NostrEvent } from '@nostr-dev-kit/ndk'
|
||||||
import { useNDKContext } from '../../hooks/useNDKContext.ts'
|
import { useNDKContext } from '../../hooks/useNDKContext.ts'
|
||||||
import { useNDK } from '../../hooks/useNDK.ts'
|
import { useNDK } from '../../hooks/useNDK.ts'
|
||||||
|
import { useImmer } from 'use-immer'
|
||||||
|
|
||||||
type FoundUser = NostrEvent & { npub: string }
|
type FoundUser = NostrEvent & { npub: string }
|
||||||
|
|
||||||
@ -94,7 +97,7 @@ export const CreatePage = () => {
|
|||||||
|
|
||||||
const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
|
const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
|
||||||
|
|
||||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([...uploadedFiles])
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const handleUploadButtonClick = () => {
|
const handleUploadButtonClick = () => {
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
@ -120,7 +123,7 @@ export const CreatePage = () => {
|
|||||||
[key: string]: NDKUserProfile
|
[key: string]: NDKUserProfile
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
|
const [drawnFiles, updateDrawnFiles] = useImmer<SigitFile[]>([])
|
||||||
const [parsingPdf, setIsParsing] = useState<boolean>(false)
|
const [parsingPdf, setIsParsing] = useState<boolean>(false)
|
||||||
|
|
||||||
const searchFieldRef = useRef<HTMLInputElement>(null)
|
const searchFieldRef = useRef<HTMLInputElement>(null)
|
||||||
@ -148,6 +151,17 @@ export const CreatePage = () => {
|
|||||||
[setUserInput]
|
[setUserInput]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleSearchUserNip05 = async (
|
||||||
|
nip05: string
|
||||||
|
): Promise<string | null> => {
|
||||||
|
const { pubkey } = await queryNip05(nip05).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
return { pubkey: null }
|
||||||
|
})
|
||||||
|
|
||||||
|
return pubkey
|
||||||
|
}
|
||||||
|
|
||||||
const handleSearchUsers = async (searchValue?: string) => {
|
const handleSearchUsers = async (searchValue?: string) => {
|
||||||
const searchString = searchValue || userSearchInput || undefined
|
const searchString = searchValue || userSearchInput || undefined
|
||||||
|
|
||||||
@ -195,8 +209,11 @@ export const CreatePage = () => {
|
|||||||
return uniqueEvents
|
return uniqueEvents
|
||||||
}, [] as FoundUser[])
|
}, [] as FoundUser[])
|
||||||
|
|
||||||
console.log('fineFilteredEvents', fineFilteredEvents)
|
console.info('fineFilteredEvents', fineFilteredEvents)
|
||||||
setFoundUsers(fineFilteredEvents)
|
setFoundUsers(fineFilteredEvents)
|
||||||
|
|
||||||
|
if (!fineFilteredEvents.length)
|
||||||
|
toast.info('No user found with the provided search term')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@ -217,7 +234,9 @@ export const CreatePage = () => {
|
|||||||
})
|
})
|
||||||
}, [foundUsers])
|
}, [foundUsers])
|
||||||
|
|
||||||
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
const handleInputKeyDown = async (
|
||||||
|
event: React.KeyboardEvent<HTMLDivElement>
|
||||||
|
) => {
|
||||||
if (
|
if (
|
||||||
event.code === KeyboardCode.Enter ||
|
event.code === KeyboardCode.Enter ||
|
||||||
event.code === KeyboardCode.NumpadEnter
|
event.code === KeyboardCode.NumpadEnter
|
||||||
@ -231,7 +250,23 @@ export const CreatePage = () => {
|
|||||||
} else {
|
} else {
|
||||||
// Otherwize if search already provided some results, user must manually click the search button
|
// Otherwize if search already provided some results, user must manually click the search button
|
||||||
if (!foundUsers.length) {
|
if (!foundUsers.length) {
|
||||||
handleSearchUsers()
|
// If it's NIP05 (includes @ or is a valid domain) send request to .well-known
|
||||||
|
const domainRegex = /^[a-zA-Z0-9@.-]+\.[a-zA-Z]{2,}$/
|
||||||
|
if (domainRegex.test(userSearchInput)) {
|
||||||
|
setSearchUsersLoading(true)
|
||||||
|
|
||||||
|
const pubkey = await handleSearchUserNip05(userSearchInput)
|
||||||
|
|
||||||
|
setSearchUsersLoading(false)
|
||||||
|
|
||||||
|
if (pubkey) {
|
||||||
|
setUserInput(userSearchInput)
|
||||||
|
} else {
|
||||||
|
toast.error(`No user found with the NIP05: ${userSearchInput}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleSearchUsers()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -248,8 +283,28 @@ export const CreatePage = () => {
|
|||||||
selectedFiles,
|
selectedFiles,
|
||||||
getSigitFile
|
getSigitFile
|
||||||
)
|
)
|
||||||
|
updateDrawnFiles((draft) => {
|
||||||
|
// Existing files are untouched
|
||||||
|
|
||||||
setDrawnFiles(files)
|
// Handle removed files
|
||||||
|
// Remove in reverse to avoid index issues
|
||||||
|
for (let i = draft.length - 1; i >= 0; i--) {
|
||||||
|
if (
|
||||||
|
!files.some(
|
||||||
|
(f) => f.name === draft[i].name && f.size === draft[i].size
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
draft.splice(i, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new files
|
||||||
|
files.forEach((f) => {
|
||||||
|
if (!draft.some((d) => d.name === f.name && d.size === f.size)) {
|
||||||
|
draft.push(f)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsParsing(true)
|
setIsParsing(true)
|
||||||
@ -258,7 +313,7 @@ export const CreatePage = () => {
|
|||||||
setIsParsing(false)
|
setIsParsing(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [selectedFiles])
|
}, [selectedFiles, updateDrawnFiles])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the drawing tool
|
* Changes the drawing tool
|
||||||
@ -296,12 +351,6 @@ export const CreatePage = () => {
|
|||||||
})
|
})
|
||||||
}, [userProfiles, users, findMetadata])
|
}, [userProfiles, users, findMetadata])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (uploadedFiles) {
|
|
||||||
setSelectedFiles([...uploadedFiles])
|
|
||||||
}
|
|
||||||
}, [uploadedFiles])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (usersPubkey) {
|
if (usersPubkey) {
|
||||||
setUsers((prev) => {
|
setUsers((prev) => {
|
||||||
@ -455,7 +504,7 @@ export const CreatePage = () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
setDrawnFiles(drawnFilesCopy)
|
updateDrawnFiles(drawnFilesCopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -479,11 +528,16 @@ export const CreatePage = () => {
|
|||||||
const files = Array.from(event.target.files)
|
const files = Array.from(event.target.files)
|
||||||
|
|
||||||
// Remove duplicates based on the file.name
|
// Remove duplicates based on the file.name
|
||||||
setSelectedFiles((p) =>
|
setSelectedFiles((p) => {
|
||||||
[...p, ...files].filter(
|
const unique = [...p, ...files].filter(
|
||||||
(file, i, array) => i === array.findIndex((t) => t.name === file.name)
|
(file, i, array) => i === array.findIndex((t) => t.name === file.name)
|
||||||
)
|
)
|
||||||
)
|
navigate('.', {
|
||||||
|
state: { uploadedFiles: unique },
|
||||||
|
replace: true
|
||||||
|
})
|
||||||
|
return unique
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,9 +551,14 @@ export const CreatePage = () => {
|
|||||||
) => {
|
) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
setSelectedFiles((prevFiles) =>
|
setSelectedFiles((prevFiles) => {
|
||||||
prevFiles.filter((file) => file.name !== fileToRemove.name)
|
const files = prevFiles.filter((file) => file.name !== fileToRemove.name)
|
||||||
)
|
navigate('.', {
|
||||||
|
state: { uploadedFiles: files },
|
||||||
|
replace: true
|
||||||
|
})
|
||||||
|
return files
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate inputs before proceeding
|
// Validate inputs before proceeding
|
||||||
@ -743,7 +802,7 @@ export const CreatePage = () => {
|
|||||||
title
|
title
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Signing nostr event for create signature')
|
setLoadingSpinnerDesc('Preparing document(s) for signing')
|
||||||
|
|
||||||
const createSignature = await signEventForMetaFile(
|
const createSignature = await signEventForMetaFile(
|
||||||
JSON.stringify(content),
|
JSON.stringify(content),
|
||||||
@ -761,7 +820,7 @@ export const CreatePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send notifications to signers and viewers
|
// Send notifications to signers and viewers
|
||||||
const sendNotifications = (meta: Meta) => {
|
const sendNotifications = (notification: SigitNotification) => {
|
||||||
// no need to send notification to self so remove it from the list
|
// no need to send notification to self so remove it from the list
|
||||||
const receivers = (
|
const receivers = (
|
||||||
signers.length > 0
|
signers.length > 0
|
||||||
@ -769,7 +828,7 @@ export const CreatePage = () => {
|
|||||||
: viewers.map((viewer) => viewer.pubkey)
|
: viewers.map((viewer) => viewer.pubkey)
|
||||||
).filter((receiver) => receiver !== usersPubkey)
|
).filter((receiver) => receiver !== usersPubkey)
|
||||||
|
|
||||||
return receivers.map((receiver) => sendNotification(receiver, meta))
|
return receivers.map((receiver) => sendNotification(receiver, notification))
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractNostrId = (stringifiedEvent: string): string => {
|
const extractNostrId = (stringifiedEvent: string): string => {
|
||||||
@ -844,11 +903,17 @@ export const CreatePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Updating user app data')
|
setLoadingSpinnerDesc('Updating user app data')
|
||||||
|
|
||||||
const event = await updateUsersAppData(meta)
|
const event = await updateUsersAppData(meta)
|
||||||
if (!event) return
|
if (!event) return
|
||||||
|
|
||||||
|
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Sending notifications to counterparties')
|
setLoadingSpinnerDesc('Sending notifications to counterparties')
|
||||||
const promises = sendNotifications(meta)
|
const promises = sendNotifications({
|
||||||
|
metaUrl,
|
||||||
|
keys: meta.keys
|
||||||
|
})
|
||||||
|
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -858,7 +923,14 @@ export const CreatePage = () => {
|
|||||||
toast.error('Failed to publish notifications')
|
toast.error('Failed to publish notifications')
|
||||||
})
|
})
|
||||||
|
|
||||||
navigate(appPrivateRoutes.sign, { state: { meta } })
|
const isFirstSigner = signers[0].pubkey === usersPubkey
|
||||||
|
|
||||||
|
if (isFirstSigner) {
|
||||||
|
navigate(appPrivateRoutes.sign, { state: { meta } })
|
||||||
|
} else {
|
||||||
|
const createSignatureJson = JSON.parse(createSignature)
|
||||||
|
navigate(`${appPublicRoutes.verify}/${createSignatureJson.id}`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const zip = new JSZip()
|
const zip = new JSZip()
|
||||||
|
|
||||||
@ -938,19 +1010,6 @@ export const CreatePage = () => {
|
|||||||
} else {
|
} else {
|
||||||
disarmAddOnEnter()
|
disarmAddOnEnter()
|
||||||
}
|
}
|
||||||
} else if (value.includes('@')) {
|
|
||||||
// Seems like it's nip05 format
|
|
||||||
const { pubkey } = await queryNip05(value).catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
return { pubkey: null }
|
|
||||||
})
|
|
||||||
|
|
||||||
if (pubkey) {
|
|
||||||
// Arm the manual user npub add after enter is hit, we don't want to trigger search
|
|
||||||
setPastedUserNpubOrNip05(hexToNpub(pubkey))
|
|
||||||
} else {
|
|
||||||
disarmAddOnEnter()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Disarm the add user on enter hit, and trigger search after 1 second
|
// Disarm the add user on enter hit, and trigger search after 1 second
|
||||||
disarmAddOnEnter()
|
disarmAddOnEnter()
|
||||||
@ -969,7 +1028,6 @@ export const CreatePage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
|
||||||
<Container className={styles.container}>
|
<Container className={styles.container}>
|
||||||
<StickySideColumns
|
<StickySideColumns
|
||||||
left={
|
left={
|
||||||
@ -1124,7 +1182,9 @@ export const CreatePage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleAddUser}
|
onClick={() => {
|
||||||
|
setUserInput(userSearchInput)
|
||||||
|
}}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
aria-label="Add"
|
aria-label="Add"
|
||||||
className={styles.counterpartToggleButton}
|
className={styles.counterpartToggleButton}
|
||||||
@ -1180,19 +1240,17 @@ export const CreatePage = () => {
|
|||||||
centerIcon={faFile}
|
centerIcon={faFile}
|
||||||
rightIcon={faToolbox}
|
rightIcon={faToolbox}
|
||||||
>
|
>
|
||||||
{parsingPdf ? (
|
<DrawPDFFields
|
||||||
<LoadingSpinner variant="small" />
|
users={users}
|
||||||
) : (
|
userProfiles={userProfiles}
|
||||||
<DrawPDFFields
|
selectedTool={selectedTool}
|
||||||
users={users}
|
sigitFiles={drawnFiles}
|
||||||
userProfiles={userProfiles}
|
updateSigitFiles={updateDrawnFiles}
|
||||||
selectedTool={selectedTool}
|
/>
|
||||||
sigitFiles={drawnFiles}
|
{parsingPdf && <LoadingSpinner variant="small" />}
|
||||||
setSigitFiles={setDrawnFiles}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</StickySideColumns>
|
</StickySideColumns>
|
||||||
</Container>
|
</Container>
|
||||||
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -135,6 +135,46 @@ export const HomePage = () => {
|
|||||||
const [filter, setFilter] = useState<Filter>('Show all')
|
const [filter, setFilter] = useState<Filter>('Show all')
|
||||||
const [sort, setSort] = useState<Sort>('desc')
|
const [sort, setSort] = useState<Sort>('desc')
|
||||||
|
|
||||||
|
const renderSubmissions = () => {
|
||||||
|
const submissions = Object.keys(parsedSigits)
|
||||||
|
.filter((s) => {
|
||||||
|
const { title, signedStatus } = parsedSigits[s]
|
||||||
|
const isMatch = title?.toLowerCase().includes(q.toLowerCase())
|
||||||
|
switch (filter) {
|
||||||
|
case 'Completed':
|
||||||
|
return signedStatus === SigitStatus.Complete && isMatch
|
||||||
|
case 'In-progress':
|
||||||
|
return signedStatus === SigitStatus.Partial && isMatch
|
||||||
|
case 'Show all':
|
||||||
|
return isMatch
|
||||||
|
default:
|
||||||
|
console.error('Filter case not handled.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const x = parsedSigits[a].createdAt ?? 0
|
||||||
|
const y = parsedSigits[b].createdAt ?? 0
|
||||||
|
return sort === 'desc' ? y - x : x - y
|
||||||
|
})
|
||||||
|
|
||||||
|
if (submissions.length) {
|
||||||
|
return submissions.map((key) => (
|
||||||
|
<DisplaySigit
|
||||||
|
key={`sigit-${key}`}
|
||||||
|
sigitCreateId={key}
|
||||||
|
parsedMeta={parsedSigits[key]}
|
||||||
|
meta={sigits[key]}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className={styles.noResults}>
|
||||||
|
<p>No results</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...getRootProps()} tabIndex={-1}>
|
<div {...getRootProps()} tabIndex={-1}>
|
||||||
<Container className={styles.container}>
|
<Container className={styles.container}>
|
||||||
@ -233,36 +273,8 @@ export const HomePage = () => {
|
|||||||
<label htmlFor="file-upload">Click or drag files to upload!</label>
|
<label htmlFor="file-upload">Click or drag files to upload!</label>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<div className={styles.submissions}>
|
|
||||||
{Object.keys(parsedSigits)
|
<div className={styles.submissions}>{renderSubmissions()}</div>
|
||||||
.filter((s) => {
|
|
||||||
const { title, signedStatus } = parsedSigits[s]
|
|
||||||
const isMatch = title?.toLowerCase().includes(q.toLowerCase())
|
|
||||||
switch (filter) {
|
|
||||||
case 'Completed':
|
|
||||||
return signedStatus === SigitStatus.Complete && isMatch
|
|
||||||
case 'In-progress':
|
|
||||||
return signedStatus === SigitStatus.Partial && isMatch
|
|
||||||
case 'Show all':
|
|
||||||
return isMatch
|
|
||||||
default:
|
|
||||||
console.error('Filter case not handled.')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
const x = parsedSigits[a].createdAt ?? 0
|
|
||||||
const y = parsedSigits[b].createdAt ?? 0
|
|
||||||
return sort === 'desc' ? y - x : x - y
|
|
||||||
})
|
|
||||||
.map((key) => (
|
|
||||||
<DisplaySigit
|
|
||||||
key={`sigit-${key}`}
|
|
||||||
sigitCreateId={key}
|
|
||||||
parsedMeta={parsedSigits[key]}
|
|
||||||
meta={sigits[key]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
@ -99,3 +99,10 @@
|
|||||||
gap: 25px;
|
gap: 25px;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(365px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(365px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.noResults {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #a1a1a1;
|
||||||
|
}
|
||||||
|
@ -1,66 +1,54 @@
|
|||||||
import { Box, Button, Typography } from '@mui/material'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import saveAs from 'file-saver'
|
import saveAs from 'file-saver'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { MuiFileInput } from 'mui-file-input'
|
|
||||||
import { Event, verifyEvent } from 'nostr-tools'
|
import { Event, verifyEvent } from 'nostr-tools'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useAppSelector } from '../../hooks/store'
|
import { useAppSelector } from '../../hooks'
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { appPublicRoutes } from '../../routes'
|
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
||||||
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
||||||
import {
|
import {
|
||||||
|
ARRAY_BUFFER,
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
|
DEFLATE,
|
||||||
encryptArrayBuffer,
|
encryptArrayBuffer,
|
||||||
extractMarksFromSignedMeta,
|
extractMarksFromSignedMeta,
|
||||||
extractZipUrlAndEncryptionKey,
|
extractZipUrlAndEncryptionKey,
|
||||||
|
filterMarksByPubkey,
|
||||||
|
findOtherUserMarks,
|
||||||
generateEncryptionKey,
|
generateEncryptionKey,
|
||||||
generateKeysFile,
|
generateKeysFile,
|
||||||
getCurrentUserFiles,
|
getCurrentUserFiles,
|
||||||
|
getCurrentUserMarks,
|
||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
isOnline,
|
isOnline,
|
||||||
loadZip,
|
loadZip,
|
||||||
unixNow,
|
|
||||||
npubToHex,
|
npubToHex,
|
||||||
parseJson,
|
parseJson,
|
||||||
|
processMarks,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
findOtherUserMarks,
|
|
||||||
timeout,
|
timeout,
|
||||||
processMarks
|
unixNow,
|
||||||
|
updateMarks,
|
||||||
|
uploadMetaToFileStorage
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { Container } from '../../components/Container'
|
|
||||||
import { DisplayMeta } from './internal/displayMeta'
|
|
||||||
import styles from './style.module.scss'
|
|
||||||
import { CurrentUserMark, Mark } from '../../types/mark.ts'
|
import { CurrentUserMark, Mark } from '../../types/mark.ts'
|
||||||
import { getLastSignersSig, isFullySigned } from '../../utils/sign.ts'
|
|
||||||
import {
|
|
||||||
filterMarksByPubkey,
|
|
||||||
getCurrentUserMarks,
|
|
||||||
isCurrentUserMarksComplete,
|
|
||||||
updateMarks
|
|
||||||
} from '../../utils'
|
|
||||||
import PdfMarking from '../../components/PDFView/PdfMarking.tsx'
|
import PdfMarking from '../../components/PDFView/PdfMarking.tsx'
|
||||||
import {
|
import {
|
||||||
convertToSigitFile,
|
convertToSigitFile,
|
||||||
getZipWithFiles,
|
getZipWithFiles,
|
||||||
SigitFile
|
SigitFile
|
||||||
} from '../../utils/file.ts'
|
} from '../../utils/file.ts'
|
||||||
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
|
|
||||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
||||||
import { useNDK } from '../../hooks/useNDK.ts'
|
import { useNDK } from '../../hooks/useNDK.ts'
|
||||||
|
import { getLastSignersSig } from '../../utils/sign.ts'
|
||||||
enum SignedStatus {
|
|
||||||
Fully_Signed,
|
|
||||||
User_Is_Next_Signer,
|
|
||||||
User_Is_Not_Next_Signer
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SignPage = () => {
|
export const SignPage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -100,17 +88,12 @@ export const SignPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [displayInput, setDisplayInput] = useState(false)
|
|
||||||
|
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
||||||
|
|
||||||
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
|
|
||||||
const [meta, setMeta] = useState<Meta | null>(null)
|
const [meta, setMeta] = useState<Meta | null>(null)
|
||||||
const [signedStatus, setSignedStatus] = useState<SignedStatus>()
|
|
||||||
|
|
||||||
const [submittedBy, setSubmittedBy] = useState<string>()
|
const [submittedBy, setSubmittedBy] = useState<string>()
|
||||||
|
|
||||||
@ -124,66 +107,14 @@ export const SignPage = () => {
|
|||||||
[key: string]: string | null
|
[key: string]: string | null
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
const [signedBy, setSignedBy] = useState<`npub1${string}`[]>([])
|
|
||||||
|
|
||||||
const [nextSinger, setNextSinger] = useState<string>()
|
|
||||||
|
|
||||||
// This state variable indicates whether the logged-in user is a signer, a creator, or neither.
|
|
||||||
const [isSignerOrCreator, setIsSignerOrCreator] = useState(false)
|
|
||||||
|
|
||||||
const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
|
const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
|
||||||
|
|
||||||
const nostrController = NostrController.getInstance()
|
const nostrController = NostrController.getInstance()
|
||||||
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>(
|
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
const [isMarksCompleted, setIsMarksCompleted] = useState(false)
|
|
||||||
const [otherUserMarks, setOtherUserMarks] = useState<Mark[]>([])
|
const [otherUserMarks, setOtherUserMarks] = useState<Mark[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (signers.length > 0) {
|
|
||||||
// check if all signers have signed then its fully signed
|
|
||||||
if (isFullySigned(signers, signedBy)) {
|
|
||||||
setSignedStatus(SignedStatus.Fully_Signed)
|
|
||||||
} else {
|
|
||||||
for (const signer of signers) {
|
|
||||||
if (!signedBy.includes(signer)) {
|
|
||||||
// signers in meta.json are in npub1 format
|
|
||||||
// so, convert it to hex before setting to nextSigner
|
|
||||||
setNextSinger(npubToHex(signer)!)
|
|
||||||
|
|
||||||
const usersNpub = hexToNpub(usersPubkey!)
|
|
||||||
|
|
||||||
if (signer === usersNpub) {
|
|
||||||
// logged in user is the next signer
|
|
||||||
setSignedStatus(SignedStatus.User_Is_Next_Signer)
|
|
||||||
} else {
|
|
||||||
setSignedStatus(SignedStatus.User_Is_Not_Next_Signer)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// there's no signer just viewers. So its fully signed
|
|
||||||
setSignedStatus(SignedStatus.Fully_Signed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine and set the status of the user
|
|
||||||
if (submittedBy && usersPubkey && submittedBy === usersPubkey) {
|
|
||||||
// If the submission was made by the user, set the status to true
|
|
||||||
setIsSignerOrCreator(true)
|
|
||||||
} else if (usersPubkey) {
|
|
||||||
// Convert the user's public key from hex to npub format
|
|
||||||
const usersNpub = hexToNpub(usersPubkey)
|
|
||||||
if (signers.includes(usersNpub)) {
|
|
||||||
// If the user's npub is in the list of signers, set the status to true
|
|
||||||
setIsSignerOrCreator(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [signers, signedBy, usersPubkey, submittedBy])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleUpdatedMeta = async (meta: Meta) => {
|
const handleUpdatedMeta = async (meta: Meta) => {
|
||||||
const createSignatureEvent = await parseJson<Event>(
|
const createSignatureEvent = await parseJson<Event>(
|
||||||
@ -263,11 +194,10 @@ export const SignPage = () => {
|
|||||||
m.value &&
|
m.value &&
|
||||||
encryptionKey
|
encryptionKey
|
||||||
) {
|
) {
|
||||||
const decrypted = await fetchAndDecrypt(
|
otherUserMarks[i].value = await fetchAndDecrypt(
|
||||||
m.value,
|
m.value,
|
||||||
encryptionKey
|
encryptionKey
|
||||||
)
|
)
|
||||||
otherUserMarks[i].value = decrypted
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error during mark fetchAndDecrypt phase`, error)
|
console.error(`Error during mark fetchAndDecrypt phase`, error)
|
||||||
@ -278,10 +208,7 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
setOtherUserMarks(otherUserMarks)
|
setOtherUserMarks(otherUserMarks)
|
||||||
setCurrentUserMarks(currentUserMarks)
|
setCurrentUserMarks(currentUserMarks)
|
||||||
setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meta) {
|
if (meta) {
|
||||||
@ -290,29 +217,6 @@ export const SignPage = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [meta, usersPubkey])
|
}, [meta, usersPubkey])
|
||||||
|
|
||||||
const handleDownload = async () => {
|
|
||||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
|
|
||||||
setLoadingSpinnerDesc('Generating file')
|
|
||||||
try {
|
|
||||||
const zip = await getZipWithFiles(meta, files)
|
|
||||||
const arrayBuffer = await zip.generateAsync({
|
|
||||||
type: ARRAY_BUFFER,
|
|
||||||
compression: DEFLATE,
|
|
||||||
compressionOptions: {
|
|
||||||
level: 6
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!arrayBuffer) return
|
|
||||||
const blob = new Blob([arrayBuffer])
|
|
||||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
|
||||||
} catch (error) {
|
|
||||||
console.log('error in zip:>> ', error)
|
|
||||||
if (error instanceof Error) {
|
|
||||||
toast.error(error.message || 'Error occurred in generating zip file')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const decrypt = useCallback(
|
const decrypt = useCallback(
|
||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
setLoadingSpinnerDesc('Decrypting file')
|
setLoadingSpinnerDesc('Decrypting file')
|
||||||
@ -424,7 +328,6 @@ export const SignPage = () => {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setDisplayInput(true)
|
|
||||||
}
|
}
|
||||||
}, [decryptedArrayBuffer, uploadedZip, metaInNavState, decrypt])
|
}, [decryptedArrayBuffer, uploadedZip, metaInNavState, decrypt])
|
||||||
|
|
||||||
@ -541,9 +444,6 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
setFiles(files)
|
setFiles(files)
|
||||||
setCurrentFileHashes(fileHashes)
|
setCurrentFileHashes(fileHashes)
|
||||||
|
|
||||||
setDisplayInput(false)
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Parsing meta.json')
|
setLoadingSpinnerDesc('Parsing meta.json')
|
||||||
|
|
||||||
const metaFileContent = await readContentOfZipEntry(
|
const metaFileContent = await readContentOfZipEntry(
|
||||||
@ -571,21 +471,6 @@ export const SignPage = () => {
|
|||||||
setMeta(parsedMetaJson)
|
setMeta(parsedMetaJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDecrypt = async () => {
|
|
||||||
if (!selectedFile) return
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
const arrayBuffer = await decrypt(selectedFile)
|
|
||||||
|
|
||||||
if (!arrayBuffer) {
|
|
||||||
setIsLoading(false)
|
|
||||||
toast.error('Error decrypting file')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDecryptedArrayBuffer(arrayBuffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSign = async () => {
|
const handleSign = async () => {
|
||||||
if (Object.entries(files).length === 0 || !meta) return
|
if (Object.entries(files).length === 0 || !meta) return
|
||||||
|
|
||||||
@ -635,11 +520,18 @@ export const SignPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (await isOnline()) {
|
if (await isOnline()) {
|
||||||
await handleOnlineFlow(updatedMeta)
|
await handleOnlineFlow(updatedMeta, encryptionKey)
|
||||||
} else {
|
} else {
|
||||||
setMeta(updatedMeta)
|
setMeta(updatedMeta)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metaInNavState) {
|
||||||
|
const createSignature = JSON.parse(metaInNavState.createSignature)
|
||||||
|
navigate(`${appPublicRoutes.verify}/${createSignature.id}`)
|
||||||
|
} else {
|
||||||
|
navigate(appPrivateRoutes.homePage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign the event for the meta file
|
// Sign the event for the meta file
|
||||||
@ -730,6 +622,14 @@ export const SignPage = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the current user is the last signer
|
||||||
|
const checkIsLastSigner = (signers: string[]): boolean => {
|
||||||
|
const usersNpub = hexToNpub(usersPubkey!)
|
||||||
|
const lastSignerIndex = signers.length - 1
|
||||||
|
const signerIndex = signers.indexOf(usersNpub)
|
||||||
|
return signerIndex === lastSignerIndex
|
||||||
|
}
|
||||||
|
|
||||||
// Handle errors during zip file generation
|
// Handle errors during zip file generation
|
||||||
const handleZipError = (err: unknown) => {
|
const handleZipError = (err: unknown) => {
|
||||||
console.log('Error in zip:>> ', err)
|
console.log('Error in zip:>> ', err)
|
||||||
@ -741,7 +641,10 @@ export const SignPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle the online flow: update users app data and send notifications
|
// Handle the online flow: update users app data and send notifications
|
||||||
const handleOnlineFlow = async (meta: Meta) => {
|
const handleOnlineFlow = async (
|
||||||
|
meta: Meta,
|
||||||
|
encryptionKey: string | undefined
|
||||||
|
) => {
|
||||||
setLoadingSpinnerDesc('Updating users app data')
|
setLoadingSpinnerDesc('Updating users app data')
|
||||||
const updatedEvent = await updateUsersAppData(meta)
|
const updatedEvent = await updateUsersAppData(meta)
|
||||||
if (!updatedEvent) {
|
if (!updatedEvent) {
|
||||||
@ -749,6 +652,18 @@ export const SignPage = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let metaUrl: string
|
||||||
|
try {
|
||||||
|
metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast.error(error.message)
|
||||||
|
}
|
||||||
|
console.error(error)
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const userSet = new Set<`npub1${string}`>()
|
const userSet = new Set<`npub1${string}`>()
|
||||||
if (submittedBy && submittedBy !== usersPubkey) {
|
if (submittedBy && submittedBy !== usersPubkey) {
|
||||||
userSet.add(hexToNpub(submittedBy))
|
userSet.add(hexToNpub(submittedBy))
|
||||||
@ -781,7 +696,7 @@ export const SignPage = () => {
|
|||||||
setLoadingSpinnerDesc('Sending notifications')
|
setLoadingSpinnerDesc('Sending notifications')
|
||||||
const users = Array.from(userSet)
|
const users = Array.from(userSet)
|
||||||
const promises = users.map((user) =>
|
const promises = users.map((user) =>
|
||||||
sendNotification(npubToHex(user)!, meta)
|
sendNotification(npubToHex(user)!, { metaUrl, keys: meta.keys })
|
||||||
)
|
)
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -795,16 +710,38 @@ export const SignPage = () => {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the current user is the last signer
|
const handleExport = async () => {
|
||||||
const checkIsLastSigner = (signers: string[]): boolean => {
|
const arrayBuffer = await prepareZipExport()
|
||||||
const usersNpub = hexToNpub(usersPubkey!)
|
if (!arrayBuffer) return
|
||||||
const lastSignerIndex = signers.length - 1
|
|
||||||
const signerIndex = signers.indexOf(usersNpub)
|
const blob = new Blob([arrayBuffer])
|
||||||
return signerIndex === lastSignerIndex
|
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
|
||||||
|
navigate(appPublicRoutes.verify)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleEncryptedExport = async () => {
|
||||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
|
const arrayBuffer = await prepareZipExport()
|
||||||
|
if (!arrayBuffer) return
|
||||||
|
|
||||||
|
const key = await generateEncryptionKey()
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Encrypting zip file')
|
||||||
|
const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key)
|
||||||
|
|
||||||
|
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
||||||
|
|
||||||
|
if (!finalZipFile) return
|
||||||
|
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prepareZipExport = async (): Promise<ArrayBuffer | null> => {
|
||||||
|
if (Object.entries(files).length === 0 || !meta || !usersPubkey)
|
||||||
|
return Promise.resolve(null)
|
||||||
|
|
||||||
const usersNpub = hexToNpub(usersPubkey)
|
const usersNpub = hexToNpub(usersPubkey)
|
||||||
if (
|
if (
|
||||||
@ -812,15 +749,15 @@ export const SignPage = () => {
|
|||||||
!viewers.includes(usersNpub) &&
|
!viewers.includes(usersNpub) &&
|
||||||
submittedBy !== usersNpub
|
submittedBy !== usersNpub
|
||||||
)
|
)
|
||||||
return
|
return Promise.resolve(null)
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
|
|
||||||
if (!meta) return
|
if (!meta) return Promise.resolve(null)
|
||||||
|
|
||||||
const prevSig = getLastSignersSig(meta, signers)
|
const prevSig = getLastSignersSig(meta, signers)
|
||||||
if (!prevSig) return
|
if (!prevSig) return Promise.resolve(null)
|
||||||
|
|
||||||
const signedEvent = await signEventForMetaFile(
|
const signedEvent = await signEventForMetaFile(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@ -830,7 +767,7 @@ export const SignPage = () => {
|
|||||||
setIsLoading
|
setIsLoading
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!signedEvent) return
|
if (!signedEvent) return Promise.resolve(null)
|
||||||
|
|
||||||
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
||||||
|
|
||||||
@ -848,8 +785,8 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
const arrayBuffer = await zip
|
const arrayBuffer = await zip
|
||||||
.generateAsync({
|
.generateAsync({
|
||||||
type: 'arraybuffer',
|
type: ARRAY_BUFFER,
|
||||||
compression: 'DEFLATE',
|
compression: DEFLATE,
|
||||||
compressionOptions: {
|
compressionOptions: {
|
||||||
level: 6
|
level: 6
|
||||||
}
|
}
|
||||||
@ -861,50 +798,9 @@ export const SignPage = () => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!arrayBuffer) return
|
if (!arrayBuffer) return Promise.resolve(null)
|
||||||
|
|
||||||
const blob = new Blob([arrayBuffer])
|
return Promise.resolve(arrayBuffer)
|
||||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
|
||||||
|
|
||||||
setIsLoading(false)
|
|
||||||
|
|
||||||
navigate(appPublicRoutes.verify)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEncryptedExport = async () => {
|
|
||||||
if (Object.entries(files).length === 0 || !meta) return
|
|
||||||
|
|
||||||
const stringifiedMeta = JSON.stringify(meta, null, 2)
|
|
||||||
const zip = await getZipWithFiles(meta, files)
|
|
||||||
|
|
||||||
zip.file('meta.json', stringifiedMeta)
|
|
||||||
|
|
||||||
const arrayBuffer = await zip
|
|
||||||
.generateAsync({
|
|
||||||
type: 'arraybuffer',
|
|
||||||
compression: 'DEFLATE',
|
|
||||||
compressionOptions: {
|
|
||||||
level: 6
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log('err in zip:>> ', err)
|
|
||||||
setIsLoading(false)
|
|
||||||
toast.error(err.message || 'Error occurred in generating zip file')
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!arrayBuffer) return
|
|
||||||
|
|
||||||
const key = await generateEncryptionKey()
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Encrypting zip file')
|
|
||||||
const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key)
|
|
||||||
|
|
||||||
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
|
||||||
|
|
||||||
if (!finalZipFile) return
|
|
||||||
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -944,90 +840,17 @@ export const SignPage = () => {
|
|||||||
return <LoadingSpinner desc={loadingSpinnerDesc} />
|
return <LoadingSpinner desc={loadingSpinnerDesc} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMarksCompleted && signedStatus === SignedStatus.User_Is_Next_Signer) {
|
|
||||||
return (
|
|
||||||
<PdfMarking
|
|
||||||
files={getCurrentUserFiles(files, currentFileHashes, creatorFileHashes)}
|
|
||||||
currentUserMarks={currentUserMarks}
|
|
||||||
setIsMarksCompleted={setIsMarksCompleted}
|
|
||||||
setCurrentUserMarks={setCurrentUserMarks}
|
|
||||||
setUpdatedMarks={setUpdatedMarks}
|
|
||||||
handleDownload={handleDownload}
|
|
||||||
otherUserMarks={otherUserMarks}
|
|
||||||
meta={meta}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PdfMarking
|
||||||
<Container className={styles.container}>
|
files={getCurrentUserFiles(files, currentFileHashes, creatorFileHashes)}
|
||||||
{displayInput && (
|
currentUserMarks={currentUserMarks}
|
||||||
<>
|
setCurrentUserMarks={setCurrentUserMarks}
|
||||||
<Typography component="label" variant="h6">
|
setUpdatedMarks={setUpdatedMarks}
|
||||||
Select sigit file
|
handleSign={handleSign}
|
||||||
</Typography>
|
handleExport={handleExport}
|
||||||
|
handleEncryptedExport={handleEncryptedExport}
|
||||||
<Box className={styles.inputBlock}>
|
otherUserMarks={otherUserMarks}
|
||||||
<MuiFileInput
|
meta={meta}
|
||||||
placeholder="Select file"
|
/>
|
||||||
inputProps={{ accept: '.sigit.zip' }}
|
|
||||||
value={selectedFile}
|
|
||||||
onChange={(value) => setSelectedFile(value)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{selectedFile && (
|
|
||||||
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Button onClick={handleDecrypt} variant="contained">
|
|
||||||
Decrypt
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{submittedBy && Object.entries(files).length > 0 && meta && (
|
|
||||||
<>
|
|
||||||
<DisplayMeta
|
|
||||||
meta={meta}
|
|
||||||
files={files}
|
|
||||||
submittedBy={submittedBy}
|
|
||||||
signers={signers}
|
|
||||||
viewers={viewers}
|
|
||||||
creatorFileHashes={creatorFileHashes}
|
|
||||||
currentFileHashes={currentFileHashes}
|
|
||||||
signedBy={signedBy}
|
|
||||||
nextSigner={nextSinger}
|
|
||||||
getPrevSignersSig={getPrevSignersSig}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{signedStatus === SignedStatus.Fully_Signed && (
|
|
||||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Button onClick={handleExport} variant="contained">
|
|
||||||
Export Sigit
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{signedStatus === SignedStatus.User_Is_Next_Signer && (
|
|
||||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Button onClick={handleSign} variant="contained">
|
|
||||||
Sign
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isSignerOrCreator && (
|
|
||||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Button onClick={handleEncryptedExport} variant="contained">
|
|
||||||
Export Encrypted Sigit
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,13 @@ import {
|
|||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
getCurrentUserFiles,
|
getCurrentUserFiles,
|
||||||
npubToHex
|
npubToHex,
|
||||||
|
generateEncryptionKey,
|
||||||
|
encryptArrayBuffer,
|
||||||
|
generateKeysFile,
|
||||||
|
ARRAY_BUFFER,
|
||||||
|
DEFLATE,
|
||||||
|
uploadMetaToFileStorage
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { useLocation, useParams } from 'react-router-dom'
|
import { useLocation, useParams } from 'react-router-dom'
|
||||||
@ -350,6 +356,11 @@ export const VerifyPage = () => {
|
|||||||
const updatedEvent = await updateUsersAppData(updatedMeta)
|
const updatedEvent = await updateUsersAppData(updatedMeta)
|
||||||
if (!updatedEvent) return
|
if (!updatedEvent) return
|
||||||
|
|
||||||
|
const metaUrl = await uploadMetaToFileStorage(
|
||||||
|
updatedMeta,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
|
||||||
const userSet = new Set<`npub1${string}`>()
|
const userSet = new Set<`npub1${string}`>()
|
||||||
signers.forEach((signer) => {
|
signers.forEach((signer) => {
|
||||||
if (signer !== usersPubkey) {
|
if (signer !== usersPubkey) {
|
||||||
@ -363,7 +374,10 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
const users = Array.from(userSet)
|
const users = Array.from(userSet)
|
||||||
const promises = users.map((user) =>
|
const promises = users.map((user) =>
|
||||||
sendNotification(npubToHex(user)!, updatedMeta)
|
sendNotification(npubToHex(user)!, {
|
||||||
|
metaUrl,
|
||||||
|
keys: meta.keys!
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
@ -540,8 +554,114 @@ export const VerifyPage = () => {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMarkedExport = async () => {
|
// Handle errors during zip file generation
|
||||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
|
const handleZipError = (err: unknown) => {
|
||||||
|
console.log('Error in zip:>> ', err)
|
||||||
|
setIsLoading(false)
|
||||||
|
if (err instanceof Error) {
|
||||||
|
toast.error(err.message || 'Error occurred in generating zip file')
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the current user is the last signer
|
||||||
|
const checkIsLastSigner = (signers: string[]): boolean => {
|
||||||
|
const usersNpub = hexToNpub(usersPubkey!)
|
||||||
|
const lastSignerIndex = signers.length - 1
|
||||||
|
const signerIndex = signers.indexOf(usersNpub)
|
||||||
|
return signerIndex === lastSignerIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// create final zip file
|
||||||
|
const createFinalZipFile = async (
|
||||||
|
encryptedArrayBuffer: ArrayBuffer,
|
||||||
|
encryptionKey: string
|
||||||
|
): Promise<File | null> => {
|
||||||
|
// Get the current timestamp in seconds
|
||||||
|
const blob = new Blob([encryptedArrayBuffer])
|
||||||
|
// Create a File object with the Blob data
|
||||||
|
const file = new File([blob], `compressed.sigit`, {
|
||||||
|
type: 'application/sigit'
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLastSigner = checkIsLastSigner(signers)
|
||||||
|
|
||||||
|
const userSet = new Set<string>()
|
||||||
|
|
||||||
|
if (isLastSigner) {
|
||||||
|
if (submittedBy) {
|
||||||
|
userSet.add(submittedBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
signers.forEach((signer) => {
|
||||||
|
userSet.add(npubToHex(signer)!)
|
||||||
|
})
|
||||||
|
|
||||||
|
viewers.forEach((viewer) => {
|
||||||
|
userSet.add(npubToHex(viewer)!)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const usersNpub = hexToNpub(usersPubkey!)
|
||||||
|
const signerIndex = signers.indexOf(usersNpub)
|
||||||
|
const nextSigner = signers[signerIndex + 1]
|
||||||
|
userSet.add(npubToHex(nextSigner)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysFileContent = await generateKeysFile(
|
||||||
|
Array.from(userSet),
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
if (!keysFileContent) return null
|
||||||
|
|
||||||
|
const zip = new JSZip()
|
||||||
|
zip.file(`compressed.sigit`, file)
|
||||||
|
zip.file('keys.json', keysFileContent)
|
||||||
|
|
||||||
|
const arraybuffer = await zip
|
||||||
|
.generateAsync({
|
||||||
|
type: 'arraybuffer',
|
||||||
|
compression: 'DEFLATE',
|
||||||
|
compressionOptions: { level: 6 }
|
||||||
|
})
|
||||||
|
.catch(handleZipError)
|
||||||
|
|
||||||
|
if (!arraybuffer) return null
|
||||||
|
|
||||||
|
return new File([new Blob([arraybuffer])], `${unixNow()}.sigit.zip`, {
|
||||||
|
type: 'application/zip'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
const arrayBuffer = await prepareZipExport()
|
||||||
|
if (!arrayBuffer) return
|
||||||
|
|
||||||
|
const blob = new Blob([arrayBuffer])
|
||||||
|
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEncryptedExport = async () => {
|
||||||
|
const arrayBuffer = await prepareZipExport()
|
||||||
|
if (!arrayBuffer) return
|
||||||
|
|
||||||
|
const key = await generateEncryptionKey()
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Encrypting zip file')
|
||||||
|
const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key)
|
||||||
|
|
||||||
|
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
||||||
|
|
||||||
|
if (!finalZipFile) return
|
||||||
|
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prepareZipExport = async (): Promise<ArrayBuffer | null> => {
|
||||||
|
if (Object.entries(files).length === 0 || !meta || !usersPubkey)
|
||||||
|
return Promise.resolve(null)
|
||||||
|
|
||||||
const usersNpub = hexToNpub(usersPubkey)
|
const usersNpub = hexToNpub(usersPubkey)
|
||||||
if (
|
if (
|
||||||
@ -549,14 +669,14 @@ export const VerifyPage = () => {
|
|||||||
!viewers.includes(usersNpub) &&
|
!viewers.includes(usersNpub) &&
|
||||||
submittedBy !== usersNpub
|
submittedBy !== usersNpub
|
||||||
) {
|
) {
|
||||||
return
|
return Promise.resolve(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
|
|
||||||
const prevSig = getLastSignersSig(meta, signers)
|
const prevSig = getLastSignersSig(meta, signers)
|
||||||
if (!prevSig) return
|
if (!prevSig) return Promise.resolve(null)
|
||||||
|
|
||||||
const signedEvent = await signEventForMetaFile(
|
const signedEvent = await signEventForMetaFile(
|
||||||
JSON.stringify({ prevSig }),
|
JSON.stringify({ prevSig }),
|
||||||
@ -564,7 +684,7 @@ export const VerifyPage = () => {
|
|||||||
setIsLoading
|
setIsLoading
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!signedEvent) return
|
if (!signedEvent) return Promise.resolve(null)
|
||||||
|
|
||||||
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
||||||
const updatedMeta = { ...meta, exportSignature }
|
const updatedMeta = { ...meta, exportSignature }
|
||||||
@ -575,8 +695,8 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
const arrayBuffer = await zip
|
const arrayBuffer = await zip
|
||||||
.generateAsync({
|
.generateAsync({
|
||||||
type: 'arraybuffer',
|
type: ARRAY_BUFFER,
|
||||||
compression: 'DEFLATE',
|
compression: DEFLATE,
|
||||||
compressionOptions: {
|
compressionOptions: {
|
||||||
level: 6
|
level: 6
|
||||||
}
|
}
|
||||||
@ -588,12 +708,9 @@ export const VerifyPage = () => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!arrayBuffer) return
|
if (!arrayBuffer) return Promise.resolve(null)
|
||||||
|
|
||||||
const blob = new Blob([arrayBuffer])
|
return Promise.resolve(arrayBuffer)
|
||||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
|
||||||
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -639,8 +756,8 @@ export const VerifyPage = () => {
|
|||||||
)}
|
)}
|
||||||
currentFile={currentFile}
|
currentFile={currentFile}
|
||||||
setCurrentFile={setCurrentFile}
|
setCurrentFile={setCurrentFile}
|
||||||
handleDownload={handleMarkedExport}
|
handleExport={handleExport}
|
||||||
downloadLabel="Download Sigit"
|
handleEncryptedExport={handleEncryptedExport}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -53,10 +53,6 @@
|
|||||||
|
|
||||||
.mark {
|
.mark {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-dev='true'] {
|
[data-dev='true'] {
|
||||||
|
@ -83,3 +83,12 @@ export interface UserAppData {
|
|||||||
export interface DocSignatureEvent extends Event {
|
export interface DocSignatureEvent extends Event {
|
||||||
parsedContent?: SignedEventContent
|
parsedContent?: SignedEventContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SigitNotification {
|
||||||
|
metaUrl: string
|
||||||
|
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSigitNotification(obj: unknown): obj is SigitNotification {
|
||||||
|
return typeof (obj as SigitNotification).metaUrl === 'string'
|
||||||
|
}
|
||||||
|
26
src/types/errors/MetaStorageError.ts
Normal file
26
src/types/errors/MetaStorageError.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Jsonable } from '.'
|
||||||
|
|
||||||
|
export enum MetaStorageErrorType {
|
||||||
|
'ENCRYPTION_KEY_REQUIRED' = 'Encryption key is required.',
|
||||||
|
'HASHING_FAILED' = "Can't get encrypted file hash.",
|
||||||
|
'FETCH_FAILED' = 'Fetching meta.json requires an encryption key.',
|
||||||
|
'HASH_VERIFICATION_FAILED' = 'Unable to verify meta.json.',
|
||||||
|
'DECRYPTION_FAILED' = 'Error decryping meta.json.',
|
||||||
|
'UNHANDLED_FETCH_ERROR' = 'Unable to fetch meta.json. Something went wrong.'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MetaStorageError extends Error {
|
||||||
|
public readonly context?: Jsonable
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: MetaStorageErrorType,
|
||||||
|
options: { cause?: Error; context?: Jsonable } = {}
|
||||||
|
) {
|
||||||
|
const { cause, context } = options
|
||||||
|
|
||||||
|
super(message, { cause })
|
||||||
|
this.name = this.constructor.name
|
||||||
|
|
||||||
|
this.context = context
|
||||||
|
}
|
||||||
|
}
|
@ -119,6 +119,6 @@ export const SIGNATURE_PAD_OPTIONS = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const SIGNATURE_PAD_SIZE = {
|
export const SIGNATURE_PAD_SIZE = {
|
||||||
width: 600,
|
width: 300,
|
||||||
height: 300
|
height: 150
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import { CreateSignatureEventContent, Meta } from '../types'
|
import { CreateSignatureEventContent, Meta } from '../types'
|
||||||
import { fromUnixTimestamp, parseJson } from '.'
|
import {
|
||||||
|
decryptArrayBuffer,
|
||||||
|
encryptArrayBuffer,
|
||||||
|
fromUnixTimestamp,
|
||||||
|
getHash,
|
||||||
|
parseJson,
|
||||||
|
uploadToFileStorage
|
||||||
|
} from '.'
|
||||||
import { Event, verifyEvent } from 'nostr-tools'
|
import { Event, verifyEvent } from 'nostr-tools'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractFileExtensions } from './file'
|
import { extractFileExtensions } from './file'
|
||||||
@ -8,6 +15,11 @@ import {
|
|||||||
MetaParseError,
|
MetaParseError,
|
||||||
MetaParseErrorType
|
MetaParseErrorType
|
||||||
} from '../types/errors/MetaParseError'
|
} from '../types/errors/MetaParseError'
|
||||||
|
import axios from 'axios'
|
||||||
|
import {
|
||||||
|
MetaStorageError,
|
||||||
|
MetaStorageErrorType
|
||||||
|
} from '../types/errors/MetaStorageError'
|
||||||
|
|
||||||
export enum SignStatus {
|
export enum SignStatus {
|
||||||
Signed = 'Signed',
|
Signed = 'Signed',
|
||||||
@ -126,3 +138,76 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const uploadMetaToFileStorage = async (
|
||||||
|
meta: Meta,
|
||||||
|
encryptionKey: string | undefined
|
||||||
|
) => {
|
||||||
|
// Value is the stringified meta object
|
||||||
|
const value = JSON.stringify(meta)
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
|
||||||
|
// Encode it to the arrayBuffer
|
||||||
|
const uint8Array = encoder.encode(value)
|
||||||
|
|
||||||
|
if (!encryptionKey) {
|
||||||
|
throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the file contents with the same encryption key from the create signature
|
||||||
|
const encryptedArrayBuffer = await encryptArrayBuffer(
|
||||||
|
uint8Array,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
|
||||||
|
const hash = await getHash(encryptedArrayBuffer)
|
||||||
|
if (!hash) {
|
||||||
|
throw new MetaStorageError(MetaStorageErrorType.HASHING_FAILED)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the encrypted json file from array buffer and hash
|
||||||
|
const file = new File([encryptedArrayBuffer], `${hash}.json`)
|
||||||
|
const url = await uploadToFileStorage(file)
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchMetaFromFileStorage = async (
|
||||||
|
url: string,
|
||||||
|
encryptionKey: string | undefined
|
||||||
|
) => {
|
||||||
|
if (!encryptionKey) {
|
||||||
|
throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED)
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedArrayBuffer = await axios.get(url, {
|
||||||
|
responseType: 'arraybuffer'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify hash
|
||||||
|
const parts = url.split('/')
|
||||||
|
const urlHash = parts[parts.length - 1]
|
||||||
|
const hash = await getHash(encryptedArrayBuffer.data)
|
||||||
|
if (hash !== urlHash) {
|
||||||
|
throw new MetaStorageError(MetaStorageErrorType.HASH_VERIFICATION_FAILED)
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await decryptArrayBuffer(
|
||||||
|
encryptedArrayBuffer.data,
|
||||||
|
encryptionKey
|
||||||
|
).catch((err) => {
|
||||||
|
throw new MetaStorageError(MetaStorageErrorType.DECRYPTION_FAILED, {
|
||||||
|
cause: err
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (arrayBuffer) {
|
||||||
|
// Decode meta.json and parse
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
const json = decoder.decode(arrayBuffer)
|
||||||
|
const meta = await parseJson<Meta>(json)
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MetaStorageError(MetaStorageErrorType.UNHANDLED_FETCH_ERROR)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user