squiggle with signature-pad and smoother lines + blossom #258

Merged
enes merged 12 commits from feat/signature-pad into staging 2024-11-26 09:35:44 +00:00
11 changed files with 352 additions and 85 deletions
Showing only changes of commit 9551750cbe - Show all commits

20
package-lock.json generated
View File

@ -48,7 +48,6 @@
"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",
"svgo": "^3.3.2",
"tseep": "1.2.1" "tseep": "1.2.1"
}, },
"devDependencies": { "devDependencies": {
@ -2215,6 +2214,7 @@
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=10.13.0" "node": ">=10.13.0"
@ -2936,6 +2936,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
@ -3635,6 +3636,7 @@
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"boolbase": "^1.0.0", "boolbase": "^1.0.0",
@ -3651,6 +3653,7 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mdn-data": "2.0.30", "mdn-data": "2.0.30",
@ -3664,6 +3667,7 @@
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
@ -3676,6 +3680,7 @@
"version": "5.0.5", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"css-tree": "~2.2.0" "css-tree": "~2.2.0"
@ -3689,6 +3694,7 @@
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mdn-data": "2.0.28", "mdn-data": "2.0.28",
@ -3703,6 +3709,7 @@
"version": "2.0.28", "version": "2.0.28",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
"dev": true,
"license": "CC0-1.0" "license": "CC0-1.0"
}, },
"node_modules/csstype": { "node_modules/csstype": {
@ -3950,6 +3957,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"domelementtype": "^2.3.0", "domelementtype": "^2.3.0",
@ -3977,6 +3985,7 @@
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -3989,6 +3998,7 @@
"version": "5.0.3", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"domelementtype": "^2.3.0" "domelementtype": "^2.3.0"
@ -4004,6 +4014,7 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"dom-serializer": "^2.0.0", "dom-serializer": "^2.0.0",
@ -4053,6 +4064,7 @@
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
@ -5980,6 +5992,7 @@
"version": "2.0.30", "version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"dev": true,
"license": "CC0-1.0" "license": "CC0-1.0"
}, },
"node_modules/merge-stream": { "node_modules/merge-stream": {
@ -6561,6 +6574,7 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"boolbase": "^1.0.0" "boolbase": "^1.0.0"
@ -6920,6 +6934,7 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
@ -7978,6 +7993,7 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -8204,6 +8220,7 @@
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@trysound/sax": "0.2.0", "@trysound/sax": "0.2.0",
@ -8229,6 +8246,7 @@
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"

View File

@ -58,7 +58,6 @@
"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",
"svgo": "^3.3.2",
"tseep": "1.2.1" "tseep": "1.2.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -6,6 +6,7 @@ import { useEffect, useRef } from 'react'
import pdfViewStyles from './style.module.scss' import pdfViewStyles from './style.module.scss'
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
import { useScale } from '../../hooks/useScale.tsx' import { useScale } from '../../hooks/useScale.tsx'
import { MARK_TYPE_CONFIG } from '../getMarkComponents.tsx'
interface PdfPageProps { interface PdfPageProps {
fileName: string fileName: string
pageIndex: number pageIndex: number
@ -60,6 +61,7 @@ const PdfPageItem = ({
/> />
))} ))}
{otherUserMarks.map((m, i) => { {otherUserMarks.map((m, i) => {
const { render: MarkRenderComponent } = MARK_TYPE_CONFIG[m.type] || {}
return ( return (
<div <div
key={i} key={i}
@ -73,7 +75,9 @@ const PdfPageItem = ({
fontSize: inPx(from(page.width, FONT_SIZE)) fontSize: inPx(from(page.width, FONT_SIZE))
}} }}
> >
{m.value} {typeof MarkRenderComponent !== 'undefined' && (
<MarkRenderComponent value={m.value} mark={m} />
)}
</div> </div>
) )
})} })}

View File

@ -1,8 +1,17 @@
import { toast } from 'react-toastify'
import { MarkType } from '../types/drawing' import { MarkType } from '../types/drawing'
import { MarkConfigs } from '../types/mark' import { MarkConfigs } from '../types/mark'
import {
decryptArrayBuffer,
encryptArrayBuffer,
getHash,
isOnline,
uploadToFileStorage
} from '../utils'
import { MarkInputSignature } from './MarkInputs/Signature' import { MarkInputSignature } from './MarkInputs/Signature'
import { MarkInputText } from './MarkInputs/Text' import { MarkInputText } from './MarkInputs/Text'
import { MarkRenderSignature } from './MarkRender/Signature' import { MarkRenderSignature } from './MarkRender/Signature'
import axios from 'axios'
export const MARK_TYPE_CONFIG: MarkConfigs = { export const MARK_TYPE_CONFIG: MarkConfigs = {
[MarkType.TEXT]: { [MarkType.TEXT]: {
@ -11,6 +20,73 @@ export const MARK_TYPE_CONFIG: MarkConfigs = {
}, },
[MarkType.SIGNATURE]: { [MarkType.SIGNATURE]: {
input: MarkInputSignature, input: MarkInputSignature,
render: MarkRenderSignature render: MarkRenderSignature,
encryptAndUpload: async (value, encryptionKey) => {
// Value is the stringified signature object
// Encode it as text to the arrayBuffer
const encoder = new TextEncoder()
const uint8Array = encoder.encode(value)
const hash = await getHash(uint8Array)
if (!hash) {
throw new Error("Can't get file hash.")
}
if (!encryptionKey) {
throw new Error('Signature requires an encryption key')
}
// Encrypt the file contents with the same encryption key from the create signature
const encryptedArrayBuffer = await encryptArrayBuffer(
uint8Array,
encryptionKey
)
// Create the encrypted json file from array buffer and hash
const file = new File([encryptedArrayBuffer], `${hash}.json`)
if (await isOnline()) {
try {
const url = await uploadToFileStorage(file)
toast.success('files.zip uploaded to file storage')
return url
} catch (error) {
if (error instanceof Error) {
toast.error(error.message || 'Error occurred in uploading file')
}
}
} else {
// Handle offline?
}
return value
},
fetchAndDecrypt: async (value, encryptionKey) => {
if (!encryptionKey) {
throw new Error('Signature requires an encryption key')
}
const encryptedArrayBuffer = await axios.get(value, {
responseType: 'arraybuffer'
})
const arrayBuffer = await decryptArrayBuffer(
encryptedArrayBuffer.data,
encryptionKey
).catch((err) => {
console.log('err in decryption:>> ', err)
return null
})
if (arrayBuffer) {
// decode json
const decoder = new TextDecoder()
const value = decoder.decode(arrayBuffer)
return value
}
// Handle offline?
return value
}
} }
} }

View File

@ -21,6 +21,7 @@ import { Event } from 'nostr-tools'
import store from '../store/store' import store from '../store/store'
import { NostrController } from '../controllers' import { NostrController } from '../controllers'
import { MetaParseError } from '../types/errors/MetaParseError' import { MetaParseError } from '../types/errors/MetaParseError'
import { MARK_TYPE_CONFIG } from '../components/getMarkComponents'
/** /**
* Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`, * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,
@ -142,6 +143,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
setMarkConfig(markConfig) setMarkConfig(markConfig)
setZipUrl(zipUrl) setZipUrl(zipUrl)
let encryptionKey: string | null = null
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
@ -162,6 +164,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
return null return null
}) })
encryptionKey = decrypted
setEncryptionKey(decrypted) setEncryptionKey(decrypted)
} }
} }
@ -206,13 +209,40 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
} }
} }
parsedSignatureEventsMap.forEach((event, npub) => { for (const [npub, event] of parsedSignatureEventsMap) {
const isValidSignature = verifyEvent(event) const isValidSignature = verifyEvent(event)
if (isValidSignature) { if (isValidSignature) {
// get the signature of prev signer from the content of current signers signedEvent // get the signature of prev signer from the content of current signers signedEvent
const prevSignersSig = getPrevSignerSig(npub) const prevSignersSig = getPrevSignerSig(npub)
try { try {
const obj: SignedEventContent = JSON.parse(event.content) const obj: SignedEventContent = JSON.parse(event.content)
// Signature object can include values that need to be fetched and decrypted
for (let i = 0; i < obj.marks.length; i++) {
const m = obj.marks[i]
try {
const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {}
if (
typeof fetchAndDecrypt === 'function' &&
m.value &&
encryptionKey
) {
const decrypted = await fetchAndDecrypt(
m.value,
encryptionKey
)
obj.marks[i].value = decrypted
}
} catch (error) {
console.error(
`Error during mark fetchAndDecrypt phase`,
error
)
}
}
parsedSignatureEventsMap.set(npub, { parsedSignatureEventsMap.set(npub, {
...event, ...event,
parsedContent: obj parsedContent: obj
@ -228,7 +258,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid) signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid)
} }
} }
}) }
signers signers
.filter((s) => !parsedSignatureEventsMap.has(s)) .filter((s) => !parsedSignatureEventsMap.has(s))

View File

@ -33,7 +33,8 @@ import {
signEventForMetaFile, signEventForMetaFile,
updateUsersAppData, updateUsersAppData,
findOtherUserMarks, findOtherUserMarks,
timeout timeout,
processMarks
} from '../../utils' } from '../../utils'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import { DisplayMeta } from './internal/displayMeta' import { DisplayMeta } from './internal/displayMeta'
@ -54,6 +55,7 @@ import {
} from '../../utils/file.ts' } from '../../utils/file.ts'
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.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/getMarkComponents.tsx'
enum SignedStatus { enum SignedStatus {
Fully_Signed, Fully_Signed,
@ -237,6 +239,43 @@ export const SignPage = () => {
const signedMarks = extractMarksFromSignedMeta(meta) const signedMarks = extractMarksFromSignedMeta(meta)
const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks) const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks)
const otherUserMarks = findOtherUserMarks(signedMarks, usersPubkey!) const otherUserMarks = findOtherUserMarks(signedMarks, usersPubkey!)
if (meta.keys) {
for (let i = 0; i < otherUserMarks.length; i++) {
const m = otherUserMarks[i]
const { sender, keys } = meta.keys
const usersNpub = hexToNpub(usersPubkey)
if (usersNpub in keys) {
const encryptionKey = await nostrController
.nip04Decrypt(sender, keys[usersNpub])
.catch((err) => {
console.log(
'An error occurred in decrypting encryption key',
err
)
return null
})
try {
const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {}
if (
typeof fetchAndDecrypt === 'function' &&
m.value &&
encryptionKey
) {
const decrypted = await fetchAndDecrypt(
m.value,
encryptionKey
)
otherUserMarks[i].value = decrypted
}
} catch (error) {
console.error(`Error during mark fetchAndDecrypt phase`, error)
}
}
}
}
setOtherUserMarks(otherUserMarks) setOtherUserMarks(otherUserMarks)
setCurrentUserMarks(currentUserMarks) setCurrentUserMarks(currentUserMarks)
setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks)) setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks))
@ -248,6 +287,7 @@ export const SignPage = () => {
if (meta) { if (meta) {
handleUpdatedMeta(meta) handleUpdatedMeta(meta)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [meta, usersPubkey]) }, [meta, usersPubkey])
const handleDownload = async () => { const handleDownload = async () => {
@ -552,8 +592,8 @@ export const SignPage = () => {
setIsLoading(true) setIsLoading(true)
setLoadingSpinnerDesc('Signing nostr event') setLoadingSpinnerDesc('Signing nostr event')
const usersNpub = hexToNpub(usersPubkey!)
const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!)) const prevSig = getPrevSignersSig(usersNpub)
if (!prevSig) { if (!prevSig) {
setIsLoading(false) setIsLoading(false)
toast.error('Previous signature is invalid') toast.error('Previous signature is invalid')
@ -562,7 +602,26 @@ export const SignPage = () => {
const marks = getSignerMarksForMeta() || [] const marks = getSignerMarksForMeta() || []
const signedEvent = await signEventForMeta({ prevSig, marks }) let encryptionKey: string | undefined
if (meta.keys) {
const { sender, keys } = meta.keys
encryptionKey = await nostrController
.nip04Decrypt(sender, keys[usersNpub])
.catch((err) => {
// Log and display an error message if decryption fails
console.log('An error occurred in decrypting encryption key', err)
toast.error('An error occurred in decrypting encryption key')
return undefined
})
}
const processedMarks = await processMarks(marks, encryptionKey)
const signedEvent = await signEventForMeta({
prevSig,
marks: processedMarks
})
if (!signedEvent) return if (!signedEvent) return
const updatedMeta = updateMetaSignatures(meta, signedEvent) const updatedMeta = updateMetaSignatures(meta, signedEvent)

View File

@ -367,82 +367,77 @@ export const VerifyPage = () => {
setIsLoading(true) setIsLoading(true)
setLoadingSpinnerDesc('Fetching file from file server') setLoadingSpinnerDesc('Fetching file from file server')
axios try {
.get(zipUrl, { const res = await axios.get(zipUrl, {
responseType: 'arraybuffer' responseType: 'arraybuffer'
}) })
.then(async (res) => {
const fileName = zipUrl.split('/').pop()
const file = new File([res.data], fileName!)
const encryptedArrayBuffer = await file.arrayBuffer() const fileName = zipUrl.split('/').pop()
const arrayBuffer = await decryptArrayBuffer( const file = new File([res.data], fileName!)
encryptedArrayBuffer,
encryptionKey const encryptedArrayBuffer = await file.arrayBuffer()
).catch((err) => { const arrayBuffer = await decryptArrayBuffer(
console.log('err in decryption:>> ', err) encryptedArrayBuffer,
encryptionKey
).catch((err) => {
console.log('err in decryption:>> ', err)
toast.error(err.message || 'An error occurred in decrypting file.')
return null
})
if (arrayBuffer) {
const zip = await JSZip.loadAsync(arrayBuffer).catch((err) => {
console.log('err in loading zip file :>> ', err)
toast.error( toast.error(
err.message || 'An error occurred in decrypting file.' err.message || 'An error occurred in loading zip file.'
) )
return null return null
}) })
if (arrayBuffer) { if (!zip) return
const zip = await JSZip.loadAsync(arrayBuffer).catch((err) => {
console.log('err in loading zip file :>> ', err)
toast.error(
err.message || 'An error occurred in loading zip file.'
)
return null
})
if (!zip) return const files: { [fileName: string]: SigitFile } = {}
const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files).map(
(entry) => entry.name
)
const files: { [fileName: string]: SigitFile } = {} // generate hashes for all entries in files folder of zipArchive
const fileHashes: { [key: string]: string | null } = {} // these hashes can be used to verify the originality of files
const fileNames = Object.values(zip.files).map( for (const fileName of fileNames) {
(entry) => entry.name const arrayBuffer = await readContentOfZipEntry(
zip,
fileName,
'arraybuffer'
) )
// generate hashes for all entries in files folder of zipArchive if (arrayBuffer) {
// these hashes can be used to verify the originality of files files[fileName] = await convertToSigitFile(
for (const fileName of fileNames) { arrayBuffer,
const arrayBuffer = await readContentOfZipEntry( fileName!
zip,
fileName,
'arraybuffer'
) )
const hash = await getHash(arrayBuffer)
if (arrayBuffer) { if (hash) {
files[fileName] = await convertToSigitFile( fileHashes[fileName.replace(/^files\//, '')] = hash
arrayBuffer,
fileName!
)
const hash = await getHash(arrayBuffer)
if (hash) {
fileHashes[fileName.replace(/^files\//, '')] = hash
}
} else {
fileHashes[fileName.replace(/^files\//, '')] = null
} }
} else {
fileHashes[fileName.replace(/^files\//, '')] = null
} }
setCurrentFileHashes(fileHashes)
setFiles(files)
setIsLoading(false)
} }
})
.catch((err) => { setCurrentFileHashes(fileHashes)
console.error(`error occurred in getting file from ${zipUrl}`, err) setFiles(files)
toast.error(
err.message || `error occurred in getting file from ${zipUrl}`
)
})
.finally(() => {
setIsLoading(false) setIsLoading(false)
}) }
} catch (err) {
const message = `error occurred in getting file from ${zipUrl}`
console.error(message, err)
if (err instanceof Error) toast.error(err.message)
else toast.error(message)
} finally {
setIsLoading(false)
}
} }
processSigit() processSigit()

View File

@ -43,7 +43,9 @@ export interface MarkRenderProps {
export interface MarkConfig { export interface MarkConfig {
input: React.FC<MarkInputProps> input: React.FC<MarkInputProps>
render?: React.FC<MarkRenderProps> render: React.FC<MarkRenderProps>
encryptAndUpload?: (value: string, key?: string) => Promise<string>
fetchAndDecrypt?: (value: string, key?: string) => Promise<string>
} }
export type MarkConfigs = { export type MarkConfigs = {

View File

@ -1,7 +1,11 @@
import { MARK_TYPE_CONFIG } from '../components/getMarkComponents.tsx'
import { NostrController } from '../controllers/NostrController.ts'
import store from '../store/store.ts'
import { Meta } from '../types' import { Meta } from '../types'
import { PdfPage } from '../types/drawing.ts' import { PdfPage } from '../types/drawing.ts'
import { MOST_COMMON_MEDIA_TYPES } from './const.ts' import { MOST_COMMON_MEDIA_TYPES } from './const.ts'
import { extractMarksFromSignedMeta } from './mark.ts' import { extractMarksFromSignedMeta } from './mark.ts'
import { hexToNpub } from './nostr.ts'
import { import {
addMarks, addMarks,
groupMarksByFileNamePage, groupMarksByFileNamePage,
@ -21,7 +25,45 @@ export const getZipWithFiles = async (
for (const [fileName, file] of Object.entries(files)) { for (const [fileName, file] of Object.entries(files)) {
// Handle PDF Files, add marks // Handle PDF Files, add marks
if (file.isPdf && fileName in marksByFileNamePage) { if (file.isPdf && fileName in marksByFileNamePage) {
const blob = await addMarks(file, marksByFileNamePage[fileName]) const marksToAdd = marksByFileNamePage[fileName]
if (meta.keys) {
for (let i = 0; i < marks.length; i++) {
const m = marks[i]
const { sender, keys } = meta.keys
const usersPubkey = store.getState().auth.usersPubkey!
const usersNpub = hexToNpub(usersPubkey)
if (usersNpub in keys) {
const encryptionKey = await NostrController.getInstance()
.nip04Decrypt(sender, keys[usersNpub])
.catch((err) => {
console.log(
'An error occurred in decrypting encryption key',
err
)
return null
})
try {
const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {}
if (
typeof fetchAndDecrypt === 'function' &&
m.value &&
encryptionKey
) {
// TODO
// extract draw with proper values
// save both pdf with marking and original hash files for signature
// ...
const decrypted = await fetchAndDecrypt(m.value, encryptionKey)
marks[i].value = decrypted
}
} catch (error) {
console.error(`Error during mark fetchAndDecrypt phase`, error)
}
}
}
}
const blob = await addMarks(file, marksToAdd)
zip.file(`marked/${fileName}`, blob) zip.file(`marked/${fileName}`, blob)
} }

View File

@ -24,6 +24,7 @@ import {
faStamp, faStamp,
faTableCellsLarge faTableCellsLarge
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { MARK_TYPE_CONFIG } from '../components/getMarkComponents.tsx'
/** /**
* Takes in an array of Marks already filtered by User. * Takes in an array of Marks already filtered by User.
@ -279,6 +280,26 @@ export const getOptimizedPathsWithStrokeWidth = (svgString: string) => {
return tuples return tuples
} }
export const processMarks = async (marks: Mark[], encryptionKey?: string) => {
const _marks = [...marks]
for (let i = 0; i < _marks.length; i++) {
const mark = _marks[i]
const hasProcess =
mark.type in MARK_TYPE_CONFIG &&
typeof MARK_TYPE_CONFIG[mark.type]?.encryptAndUpload === 'function'
if (hasProcess) {
const value = mark.value!
const processFn = MARK_TYPE_CONFIG[mark.type]?.encryptAndUpload
if (processFn) {
mark.value = await processFn(value, encryptionKey)
}
}
}
return _marks
}
export { export {
getCurrentUserMarks, getCurrentUserMarks,
filterMarksByPubkey, filterMarksByPubkey,

View File

@ -1,5 +1,5 @@
import { MarkType, PdfPage } from '../types/drawing.ts' import { MarkType, PdfPage } from '../types/drawing.ts'
import { LineCapStyle, PDFDocument, PDFFont, PDFPage, rgb } from 'pdf-lib' import { PDFDocument, PDFFont, PDFPage, rgb } from 'pdf-lib'
import { Mark } from '../types/mark.ts' import { Mark } from '../types/mark.ts'
import * as PDFJS from 'pdfjs-dist' import * as PDFJS from 'pdfjs-dist'
import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker' import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker'
@ -11,6 +11,9 @@ if (!PDFJS.GlobalWorkerOptions.workerPort) {
import fontkit from '@pdf-lib/fontkit' import fontkit from '@pdf-lib/fontkit'
import defaultFont from '../assets/fonts/roboto-regular.ttf' import defaultFont from '../assets/fonts/roboto-regular.ttf'
import { BasicPoint } from 'signature_pad/dist/types/point'
import SignaturePad from 'signature_pad'
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from './const.ts'
/** /**
* Defined font size used when generating a PDF. Currently it is difficult to fully * Defined font size used when generating a PDF. Currently it is difficult to fully
@ -136,7 +139,7 @@ export const addMarks = async (
const mark = marksPerPage[i][j] const mark = marksPerPage[i][j]
switch (mark.type) { switch (mark.type) {
case MarkType.SIGNATURE: case MarkType.SIGNATURE:
drawSignatureText(mark, pages[i]) await embedSignaturePng(mark, pages[i], pdf)
break break
default: default:
@ -255,23 +258,41 @@ async function embedFont(pdf: PDFDocument) {
return embeddedFont return embeddedFont
} }
const drawSignatureText = (mark: Mark, page: PDFPage) => { const embedSignaturePng = async (
mark: Mark,
page: PDFPage,
pdf: PDFDocument
) => {
const { location } = mark const { location } = mark
const { height } = page.getSize() const { height } = page.getSize()
// Convert the mark location origin (top, left) to PDF origin (bottom, left)
const x = location.left
const y = height - location.top
if (hasValue(mark)) { if (hasValue(mark)) {
const segments: string[][] = JSON.parse(mark.value!) const data = JSON.parse(mark.value!).map((p: BasicPoint[]) => ({
segments.forEach(([d, w]) => { points: p
page.drawSvgPath(d, { }))
x, const canvas = document.createElement('canvas')
y, canvas.width = SIGNATURE_PAD_SIZE.width
borderWidth: parseFloat(w), canvas.height = SIGNATURE_PAD_SIZE.height
borderLineCap: LineCapStyle.Round const pad = new SignaturePad(canvas, SIGNATURE_PAD_OPTIONS)
}) pad.fromData(data)
const signatureImage = await pdf.embedPng(pad.toDataURL())
const scaled = signatureImage.scaleToFit(location.width, location.height)
// Convert the mark location origin (top, left) to PDF origin (bottom, left)
// and center the image
const x = location.left + (location.width - scaled.width) / 2
const y =
height -
location.top -
location.height +
(location.height - scaled.height) / 2
page.drawImage(signatureImage, {
x,
y,
width: scaled.width,
height: scaled.height
}) })
} }
} }