chore(git): Merge branch 'staging' into issue-232
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
This commit is contained in:
commit
22debeb033
@ -6,7 +6,7 @@ module.exports = {
|
|||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:react-hooks/recommended'
|
'plugin:react-hooks/recommended'
|
||||||
],
|
],
|
||||||
ignorePatterns: ['dist', '.eslintrc.cjs', 'licenseChecker.cjs'],
|
ignorePatterns: ['dist', '.eslintrc.cjs', 'licenseChecker.cjs', "*.min.js"],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
plugins: ['react-refresh'],
|
plugins: ['react-refresh'],
|
||||||
rules: {
|
rules: {
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script src="/opentimestamps.min.js"></script>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
1633
package-lock.json
generated
1633
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -57,6 +57,7 @@
|
|||||||
"react-singleton-hook": "^4.0.1",
|
"react-singleton-hook": "^4.0.1",
|
||||||
"react-toastify": "10.0.4",
|
"react-toastify": "10.0.4",
|
||||||
"redux": "5.0.1",
|
"redux": "5.0.1",
|
||||||
|
"svgo": "^3.3.2",
|
||||||
"tseep": "1.2.1"
|
"tseep": "1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -66,6 +67,7 @@
|
|||||||
"@types/pdfjs-dist": "^2.10.378",
|
"@types/pdfjs-dist": "^2.10.378",
|
||||||
"@types/react": "^18.2.56",
|
"@types/react": "^18.2.56",
|
||||||
"@types/react-dom": "^18.2.19",
|
"@types/react-dom": "^18.2.19",
|
||||||
|
"@types/svgo": "^3.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||||
"@typescript-eslint/parser": "^7.0.2",
|
"@typescript-eslint/parser": "^7.0.2",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
@ -78,6 +80,7 @@
|
|||||||
"ts-css-modules-vite-plugin": "1.0.20",
|
"ts-css-modules-vite-plugin": "1.0.20",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.1.4",
|
"vite": "^5.1.4",
|
||||||
|
"vite-plugin-node-polyfills": "^0.22.0",
|
||||||
"vite-tsconfig-paths": "4.3.2"
|
"vite-tsconfig-paths": "4.3.2"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"names": {
|
"names": {
|
||||||
"_": "6bf1024c6336093b632db2da21b65a44c7d82830454fb4d75634ba281e161c90"
|
"_": "6bf1024c6336093b632db2da21b65a44c7d82830454fb4d75634ba281e161c90"
|
||||||
},
|
},
|
||||||
"relays": {
|
"relays": {
|
||||||
"6bf1024c6336093b632db2da21b65a44c7d82830454fb4d75634ba281e161c90": [
|
"6bf1024c6336093b632db2da21b65a44c7d82830454fb4d75634ba281e161c90": [
|
||||||
"wss://brb.io",
|
"wss://brb.io",
|
||||||
"wss://nostr.v0l.io",
|
"wss://nostr.v0l.io",
|
||||||
"wss://nostr.coinos.io",
|
"wss://nostr.coinos.io",
|
||||||
"wss://rsslay.nostr.net",
|
"wss://rsslay.nostr.net",
|
||||||
"wss://relay.current.fyi",
|
"wss://relay.current.fyi",
|
||||||
"wss://nos.io"
|
"wss://nos.io"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
2
public/opentimestamps.min.js
vendored
Normal file
2
public/opentimestamps.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -9,7 +9,7 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { ProfileMetadata, User, UserRole } from '../../types'
|
import { ProfileMetadata, User, UserRole, KeyboardCode } from '../../types'
|
||||||
import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
|
import { MouseState, PdfPage, 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'
|
||||||
@ -27,6 +27,10 @@ const DEFAULT_START_SIZE = {
|
|||||||
height: 40
|
height: 40
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
interface HideSignersForDrawnField {
|
||||||
|
[key: number]: boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
users: User[]
|
users: User[]
|
||||||
metadata: { [key: string]: ProfileMetadata }
|
metadata: { [key: string]: ProfileMetadata }
|
||||||
@ -41,6 +45,9 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
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) : ''
|
||||||
const [lastSigner, setLastSigner] = useState(defaultSignerNpub)
|
const [lastSigner, setLastSigner] = useState(defaultSignerNpub)
|
||||||
|
const [hideSignersForDrawnField, setHideSignersForDrawnField] =
|
||||||
|
useState<HideSignersForDrawnField>({})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return first pubkey that is present in the signers list
|
* Return first pubkey that is present in the signers list
|
||||||
* @param pubkeys
|
* @param pubkeys
|
||||||
@ -217,6 +224,12 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
y: drawingRectangleCoords.y
|
y: drawingRectangleCoords.y
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// make signers dropdown visible
|
||||||
|
setHideSignersForDrawnField((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[drawnFieldIndex]: false
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -338,6 +351,32 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles Escape button-down event and hides all signers dropdowns
|
||||||
|
* @param event SyntheticEvent event
|
||||||
|
*/
|
||||||
|
const handleEscapeButtonDown = (event: React.SyntheticEvent) => {
|
||||||
|
// get native event
|
||||||
|
const { nativeEvent } = event
|
||||||
|
|
||||||
|
//check if event is a keyboard event
|
||||||
|
if (nativeEvent instanceof KeyboardEvent) {
|
||||||
|
// check if event code is Escape
|
||||||
|
if (nativeEvent.code === KeyboardCode.Escape) {
|
||||||
|
// hide all signers dropdowns
|
||||||
|
const newHideSignersForDrawnField: HideSignersForDrawnField = {}
|
||||||
|
|
||||||
|
Object.keys(hideSignersForDrawnField).forEach((key) => {
|
||||||
|
// Object.keys always returns an array of strings,
|
||||||
|
// that is why unknown type is used below
|
||||||
|
newHideSignersForDrawnField[key as unknown as number] = true
|
||||||
|
})
|
||||||
|
|
||||||
|
setHideSignersForDrawnField(newHideSignersForDrawnField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the pointer coordinates relative to a element in the `event` param
|
* Gets the pointer coordinates relative to a element in the `event` param
|
||||||
* @param event PointerEvent
|
* @param event PointerEvent
|
||||||
@ -361,6 +400,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
rect
|
rect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the pdf pages and drawing elements
|
* Renders the pdf pages and drawing elements
|
||||||
*/
|
*/
|
||||||
@ -375,6 +415,8 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
<div
|
<div
|
||||||
key={pageIndex}
|
key={pageIndex}
|
||||||
className={`image-wrapper ${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`}
|
className={`image-wrapper ${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`}
|
||||||
|
tabIndex={-1}
|
||||||
|
onKeyDown={(event) => handleEscapeButtonDown(event)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
onPointerMove={(event) => {
|
onPointerMove={(event) => {
|
||||||
@ -492,62 +534,78 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
fileIndex,
|
fileIndex,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
drawnFieldIndex
|
drawnFieldIndex
|
||||||
) && (
|
) &&
|
||||||
<div
|
(!hideSignersForDrawnField ||
|
||||||
onPointerDown={handleUserSelectPointerDown}
|
!hideSignersForDrawnField[drawnFieldIndex]) && (
|
||||||
className={styles.userSelect}
|
<div
|
||||||
>
|
onPointerDown={handleUserSelectPointerDown}
|
||||||
<FormControl fullWidth size="small">
|
className={styles.userSelect}
|
||||||
<InputLabel id="counterparts">Counterpart</InputLabel>
|
>
|
||||||
<Select
|
<FormControl fullWidth size="small">
|
||||||
value={getAvailableSigner(drawnField.counterpart)}
|
<InputLabel id="counterparts">
|
||||||
onChange={(event) => {
|
Counterpart
|
||||||
drawnField.counterpart = event.target.value
|
</InputLabel>
|
||||||
setLastSigner(event.target.value)
|
<Select
|
||||||
refreshPdfFiles()
|
value={getAvailableSigner(drawnField.counterpart)}
|
||||||
}}
|
onChange={(event) => {
|
||||||
labelId="counterparts"
|
drawnField.counterpart = event.target.value
|
||||||
label="Counterparts"
|
setLastSigner(event.target.value)
|
||||||
sx={{
|
refreshPdfFiles()
|
||||||
background: 'white'
|
}}
|
||||||
}}
|
labelId="counterparts"
|
||||||
renderValue={(value) =>
|
label="Counterparts"
|
||||||
renderCounterpartValue(value)
|
sx={{
|
||||||
}
|
background: 'white'
|
||||||
>
|
}}
|
||||||
{signers.map((signer, index) => {
|
renderValue={(value) =>
|
||||||
const npub = hexToNpub(signer.pubkey)
|
renderCounterpartValue(value)
|
||||||
const metadata = props.metadata[signer.pubkey]
|
}
|
||||||
const displayValue = getProfileUsername(
|
>
|
||||||
npub,
|
{signers.map((signer, index) => {
|
||||||
metadata
|
const npub = hexToNpub(signer.pubkey)
|
||||||
)
|
const metadata = props.metadata[signer.pubkey]
|
||||||
|
const displayValue = getProfileUsername(
|
||||||
|
npub,
|
||||||
|
metadata
|
||||||
|
)
|
||||||
|
// make current signers dropdown visible
|
||||||
|
if (
|
||||||
|
hideSignersForDrawnField[drawnFieldIndex] ===
|
||||||
|
undefined ||
|
||||||
|
hideSignersForDrawnField[drawnFieldIndex] ===
|
||||||
|
true
|
||||||
|
) {
|
||||||
|
setHideSignersForDrawnField((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[drawnFieldIndex]: false
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem key={index} value={npub}>
|
<MenuItem key={index} value={npub}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<AvatarIconButton
|
<AvatarIconButton
|
||||||
src={metadata?.picture}
|
src={metadata?.picture}
|
||||||
hexKey={signer.pubkey}
|
hexKey={signer.pubkey}
|
||||||
aria-label={`account of user ${displayValue}`}
|
aria-label={`account of user ${displayValue}`}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
sx={{
|
sx={{
|
||||||
padding: 0,
|
padding: 0,
|
||||||
'> img': {
|
'> img': {
|
||||||
width: '30px',
|
width: '30px',
|
||||||
height: '30px'
|
height: '30px'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{displayValue}</ListItemText>
|
<ListItemText>{displayValue}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -13,6 +13,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdfImageWrapper:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -7,13 +7,12 @@ import {
|
|||||||
isCurrentValueLast
|
isCurrentValueLast
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { MARK_TYPE_CONFIG } from '../getMarkComponents.tsx'
|
||||||
|
|
||||||
interface MarkFormFieldProps {
|
interface MarkFormFieldProps {
|
||||||
currentUserMarks: CurrentUserMark[]
|
currentUserMarks: CurrentUserMark[]
|
||||||
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
|
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
|
||||||
handleSelectedMarkValueChange: (
|
handleSelectedMarkValueChange: (value: string) => void
|
||||||
event: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => void
|
|
||||||
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||||
selectedMark: CurrentUserMark
|
selectedMark: CurrentUserMark
|
||||||
selectedMarkValue: string
|
selectedMarkValue: string
|
||||||
@ -53,6 +52,8 @@ const MarkFormField = ({
|
|||||||
}
|
}
|
||||||
const toggleActions = () => setDisplayActions(!displayActions)
|
const toggleActions = () => setDisplayActions(!displayActions)
|
||||||
const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type)
|
const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type)
|
||||||
|
const { input: MarkInputComponent } =
|
||||||
|
MARK_TYPE_CONFIG[selectedMark.mark.type] || {}
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.trigger}>
|
<div className={styles.trigger}>
|
||||||
@ -83,12 +84,14 @@ const MarkFormField = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.inputWrapper}>
|
<div className={styles.inputWrapper}>
|
||||||
<form onSubmit={(e) => handleFormSubmit(e)}>
|
<form onSubmit={(e) => handleFormSubmit(e)}>
|
||||||
<input
|
{typeof MarkInputComponent !== 'undefined' && (
|
||||||
className={styles.input}
|
<MarkInputComponent
|
||||||
placeholder={markLabel}
|
value={selectedMarkValue}
|
||||||
onChange={handleSelectedMarkValueChange}
|
placeholder={markLabel}
|
||||||
value={selectedMarkValue}
|
handler={handleSelectedMarkValueChange}
|
||||||
/>
|
userMark={selectedMark}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className={styles.actionsBottom}>
|
<div className={styles.actionsBottom}>
|
||||||
<button type="submit" className={styles.submitButton}>
|
<button type="submit" className={styles.submitButton}>
|
||||||
NEXT
|
NEXT
|
||||||
|
44
src/components/MarkInputs/Signature.module.scss
Normal file
44
src/components/MarkInputs/Signature.module.scss
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
|
$padding: 5px;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: $padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
outline: 1px solid black;
|
||||||
|
background-color: $body-background-color;
|
||||||
|
cursor: crosshair;
|
||||||
|
|
||||||
|
// Disable panning/zooming when touching canvas element
|
||||||
|
-ms-touch-action: none;
|
||||||
|
touch-action: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: $padding;
|
||||||
|
color: $primary-main;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-dark;
|
||||||
|
}
|
||||||
|
}
|
101
src/components/MarkInputs/Signature.tsx
Normal file
101
src/components/MarkInputs/Signature.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { useRef, useState } from 'react'
|
||||||
|
import { MarkInputProps } from '../../types/mark'
|
||||||
|
import { getOptimizedPaths, optimizeSVG } from '../../utils'
|
||||||
|
import { faEraser } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import styles from './Signature.module.scss'
|
||||||
|
import { MarkRenderSignature } from '../MarkRender/Signature'
|
||||||
|
|
||||||
|
export const MarkInputSignature = ({
|
||||||
|
value,
|
||||||
|
handler,
|
||||||
|
userMark
|
||||||
|
}: MarkInputProps) => {
|
||||||
|
const location = userMark?.mark.location
|
||||||
|
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const [drawing, setDrawing] = useState(false)
|
||||||
|
const [paths, setPaths] = useState<string[]>(value ? JSON.parse(value) : [])
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
if (location && paths) {
|
||||||
|
if (paths.length) {
|
||||||
|
const optimizedSvg = optimizeSVG(location, paths)
|
||||||
|
const extractedPaths = getOptimizedPaths(optimizedSvg)
|
||||||
|
handler(JSON.stringify(extractedPaths))
|
||||||
|
} else {
|
||||||
|
handler('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePointerDown = (event: React.PointerEvent) => {
|
||||||
|
const rect = event.currentTarget.getBoundingClientRect()
|
||||||
|
const x = event.clientX - rect.left
|
||||||
|
const y = event.clientY - rect.top
|
||||||
|
|
||||||
|
const ctx = canvasRef.current?.getContext('2d')
|
||||||
|
ctx?.beginPath()
|
||||||
|
ctx?.moveTo(x, y)
|
||||||
|
setPaths([...paths, `M ${x} ${y}`])
|
||||||
|
setDrawing(true)
|
||||||
|
}
|
||||||
|
const handlePointerUp = () => {
|
||||||
|
setDrawing(false)
|
||||||
|
update()
|
||||||
|
const ctx = canvasRef.current?.getContext('2d')
|
||||||
|
ctx?.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height)
|
||||||
|
}
|
||||||
|
const handlePointerMove = (event: React.PointerEvent) => {
|
||||||
|
if (!drawing) return
|
||||||
|
const ctx = canvasRef.current?.getContext('2d')
|
||||||
|
const rect = canvasRef.current?.getBoundingClientRect()
|
||||||
|
const x = event.clientX - rect!.left
|
||||||
|
const y = event.clientY - rect!.top
|
||||||
|
|
||||||
|
ctx?.lineTo(x, y)
|
||||||
|
ctx?.stroke()
|
||||||
|
|
||||||
|
// Collect the path data
|
||||||
|
setPaths((prevPaths) => {
|
||||||
|
const newPaths = [...prevPaths]
|
||||||
|
newPaths[newPaths.length - 1] += ` L ${x} ${y}`
|
||||||
|
return newPaths
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setPaths([])
|
||||||
|
setDrawing(false)
|
||||||
|
update()
|
||||||
|
const ctx = canvasRef.current?.getContext('2d')
|
||||||
|
ctx?.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<div className={styles.relative}>
|
||||||
|
<canvas
|
||||||
|
height={location?.height}
|
||||||
|
width={location?.width}
|
||||||
|
ref={canvasRef}
|
||||||
|
className={styles.canvas}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onPointerMove={handlePointerMove}
|
||||||
|
onPointerOut={handlePointerUp}
|
||||||
|
></canvas>
|
||||||
|
{typeof userMark?.mark !== 'undefined' && (
|
||||||
|
<div className={styles.absolute}>
|
||||||
|
<MarkRenderSignature value={value} mark={userMark.mark} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.reset}>
|
||||||
|
<FontAwesomeIcon size="sm" icon={faEraser} onClick={handleReset} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
19
src/components/MarkInputs/Text.tsx
Normal file
19
src/components/MarkInputs/Text.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { MarkInputProps } from '../../types/mark'
|
||||||
|
import styles from '../MarkFormField/style.module.scss'
|
||||||
|
|
||||||
|
export const MarkInputText = ({
|
||||||
|
value,
|
||||||
|
handler,
|
||||||
|
placeholder
|
||||||
|
}: MarkInputProps) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className={styles.input}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(e) => {
|
||||||
|
handler(e.currentTarget.value)
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
13
src/components/MarkRender/Signature.tsx
Normal file
13
src/components/MarkRender/Signature.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { MarkRenderProps } from '../../types/mark'
|
||||||
|
|
||||||
|
export const MarkRenderSignature = ({ value, mark }: MarkRenderProps) => {
|
||||||
|
const paths = value ? JSON.parse(value) : []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg viewBox={`0 0 ${mark.location.width} ${mark.location.height}`}>
|
||||||
|
{paths.map((path: string) => (
|
||||||
|
<path d={path} stroke="black" fill="none" />
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
@ -4,6 +4,7 @@ import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
|||||||
import { useScale } from '../../hooks/useScale.tsx'
|
import { useScale } from '../../hooks/useScale.tsx'
|
||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
import { npubToHex } from '../../utils/nostr.ts'
|
import { npubToHex } from '../../utils/nostr.ts'
|
||||||
|
import { MARK_TYPE_CONFIG } from '../getMarkComponents.tsx'
|
||||||
|
|
||||||
interface PdfMarkItemProps {
|
interface PdfMarkItemProps {
|
||||||
userMark: CurrentUserMark
|
userMark: CurrentUserMark
|
||||||
@ -27,6 +28,8 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
|
|||||||
const getMarkValue = () =>
|
const getMarkValue = () =>
|
||||||
isEdited() ? selectedMarkValue : userMark.currentValue
|
isEdited() ? selectedMarkValue : userMark.currentValue
|
||||||
const { from } = useScale()
|
const { from } = useScale()
|
||||||
|
const { render: MarkRenderComponent } =
|
||||||
|
MARK_TYPE_CONFIG[userMark.mark.type] || {}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -47,7 +50,9 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
|
|||||||
fontSize: inPx(from(pageWidth, FONT_SIZE))
|
fontSize: inPx(from(pageWidth, FONT_SIZE))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{getMarkValue()}
|
{typeof MarkRenderComponent !== 'undefined' && (
|
||||||
|
<MarkRenderComponent value={getMarkValue()} mark={userMark.mark} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -117,8 +117,7 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
// setCurrentUserMarks(updatedCurrentUserMarks)
|
// setCurrentUserMarks(updatedCurrentUserMarks)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) =>
|
const handleChange = (value: string) => setSelectedMarkValue(value)
|
||||||
setSelectedMarkValue(event.target.value)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
fromUnixTimestamp,
|
fromUnixTimestamp,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
|
SigitStatus,
|
||||||
SignStatus
|
SignStatus
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
||||||
@ -15,6 +16,8 @@ import {
|
|||||||
faCalendar,
|
faCalendar,
|
||||||
faCalendarCheck,
|
faCalendarCheck,
|
||||||
faCalendarPlus,
|
faCalendarPlus,
|
||||||
|
faCheck,
|
||||||
|
faClock,
|
||||||
faEye,
|
faEye,
|
||||||
faFile,
|
faFile,
|
||||||
faFileCircleExclamation
|
faFileCircleExclamation
|
||||||
@ -22,7 +25,7 @@ import {
|
|||||||
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
||||||
import { useAppSelector } from '../../hooks/store'
|
import { useAppSelector } from '../../hooks/store'
|
||||||
import { DisplaySigner } from '../DisplaySigner'
|
import { DisplaySigner } from '../DisplaySigner'
|
||||||
import { Meta } from '../../types'
|
import { Meta, OpenTimestamp } from '../../types'
|
||||||
import { extractFileExtensions } from '../../utils/file'
|
import { extractFileExtensions } from '../../utils/file'
|
||||||
import { UserAvatar } from '../UserAvatar'
|
import { UserAvatar } from '../UserAvatar'
|
||||||
|
|
||||||
@ -42,7 +45,9 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
completedAt,
|
completedAt,
|
||||||
parsedSignatureEvents,
|
parsedSignatureEvents,
|
||||||
signedStatus,
|
signedStatus,
|
||||||
isValid
|
isValid,
|
||||||
|
id,
|
||||||
|
timestamps
|
||||||
} = useSigitMeta(meta)
|
} = useSigitMeta(meta)
|
||||||
const { usersPubkey } = useAppSelector((state) => state.auth)
|
const { usersPubkey } = useAppSelector((state) => state.auth)
|
||||||
const userCanSign =
|
const userCanSign =
|
||||||
@ -51,6 +56,50 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
|
|
||||||
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
|
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
|
||||||
|
|
||||||
|
const isTimestampVerified = (
|
||||||
|
timestamps: OpenTimestamp[],
|
||||||
|
nostrId: string
|
||||||
|
): boolean => {
|
||||||
|
const matched = timestamps.find((t) => t.nostrId === nostrId)
|
||||||
|
return !!(matched && matched.verification)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOpenTimestampsInfo = (
|
||||||
|
timestamps: OpenTimestamp[],
|
||||||
|
nostrId: string
|
||||||
|
) => {
|
||||||
|
if (isTimestampVerified(timestamps, nostrId)) {
|
||||||
|
return <FontAwesomeIcon className={styles.ticket} icon={faCheck} />
|
||||||
|
} else {
|
||||||
|
return <FontAwesomeIcon className={styles.ticket} icon={faClock} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCompletedOpenTimestampsInfo = (timestamp: OpenTimestamp) => {
|
||||||
|
if (timestamp.verification) {
|
||||||
|
return <FontAwesomeIcon className={styles.ticket} icon={faCheck} />
|
||||||
|
} else {
|
||||||
|
return <FontAwesomeIcon className={styles.ticket} icon={faClock} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTimestampTooltipTitle = (label: string, isVerified: boolean) => {
|
||||||
|
return `${label} / Open Timestamp ${isVerified ? 'Verified' : 'Pending'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserSignatureTimestampVerified = () => {
|
||||||
|
if (
|
||||||
|
userCanSign &&
|
||||||
|
hexToNpub(usersPubkey) in parsedSignatureEvents &&
|
||||||
|
timestamps &&
|
||||||
|
timestamps.length > 0
|
||||||
|
) {
|
||||||
|
const nostrId = parsedSignatureEvents[hexToNpub(usersPubkey)].id
|
||||||
|
return isTimestampVerified(timestamps, nostrId)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return submittedBy ? (
|
return submittedBy ? (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
@ -115,19 +164,35 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
<p>Details</p>
|
<p>Details</p>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={'Publication date'}
|
title={getTimestampTooltipTitle(
|
||||||
|
'Publication date',
|
||||||
|
!!(timestamps && id && isTimestampVerified(timestamps, id))
|
||||||
|
)}
|
||||||
placement="top"
|
placement="top"
|
||||||
arrow
|
arrow
|
||||||
disableInteractive
|
disableInteractive
|
||||||
>
|
>
|
||||||
<span className={styles.detailsItem}>
|
<span className={styles.detailsItem}>
|
||||||
<FontAwesomeIcon icon={faCalendarPlus} />{' '}
|
<FontAwesomeIcon icon={faCalendarPlus} />{' '}
|
||||||
{createdAt ? formatTimestamp(createdAt) : <>—</>}
|
{createdAt ? formatTimestamp(createdAt) : <>—</>}{' '}
|
||||||
|
{timestamps &&
|
||||||
|
timestamps.length > 0 &&
|
||||||
|
id &&
|
||||||
|
getOpenTimestampsInfo(timestamps, id)}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={'Completion date'}
|
title={getTimestampTooltipTitle(
|
||||||
|
'Completion date',
|
||||||
|
!!(
|
||||||
|
signedStatus === SigitStatus.Complete &&
|
||||||
|
completedAt &&
|
||||||
|
timestamps &&
|
||||||
|
timestamps.length > 0 &&
|
||||||
|
timestamps[timestamps.length - 1].verification
|
||||||
|
)
|
||||||
|
)}
|
||||||
placement="top"
|
placement="top"
|
||||||
arrow
|
arrow
|
||||||
disableInteractive
|
disableInteractive
|
||||||
@ -135,13 +200,26 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
<span className={styles.detailsItem}>
|
<span className={styles.detailsItem}>
|
||||||
<FontAwesomeIcon icon={faCalendarCheck} />{' '}
|
<FontAwesomeIcon icon={faCalendarCheck} />{' '}
|
||||||
{completedAt ? formatTimestamp(completedAt) : <>—</>}
|
{completedAt ? formatTimestamp(completedAt) : <>—</>}
|
||||||
|
{signedStatus === SigitStatus.Complete &&
|
||||||
|
completedAt &&
|
||||||
|
timestamps &&
|
||||||
|
timestamps.length > 0 && (
|
||||||
|
<span className={styles.ticket}>
|
||||||
|
{getCompletedOpenTimestampsInfo(
|
||||||
|
timestamps[timestamps.length - 1]
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* User signed date */}
|
{/* User signed date */}
|
||||||
{userCanSign ? (
|
{userCanSign ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={'Your signature date'}
|
title={getTimestampTooltipTitle(
|
||||||
|
'Your signature date',
|
||||||
|
isUserSignatureTimestampVerified()
|
||||||
|
)}
|
||||||
placement="top"
|
placement="top"
|
||||||
arrow
|
arrow
|
||||||
disableInteractive
|
disableInteractive
|
||||||
@ -161,6 +239,16 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<>—</>
|
<>—</>
|
||||||
)}
|
)}
|
||||||
|
{hexToNpub(usersPubkey) in parsedSignatureEvents &&
|
||||||
|
timestamps &&
|
||||||
|
timestamps.length > 0 && (
|
||||||
|
<span className={styles.ticket}>
|
||||||
|
{getOpenTimestampsInfo(
|
||||||
|
timestamps,
|
||||||
|
parsedSignatureEvents[hexToNpub(usersPubkey)].id
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -31,8 +31,6 @@
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: start;
|
|
||||||
|
|
||||||
> :first-child {
|
> :first-child {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@ -44,3 +42,7 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ticket {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
16
src/components/getMarkComponents.tsx
Normal file
16
src/components/getMarkComponents.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { MarkType } from '../types/drawing'
|
||||||
|
import { MarkConfigs } from '../types/mark'
|
||||||
|
import { MarkInputSignature } from './MarkInputs/Signature'
|
||||||
|
import { MarkInputText } from './MarkInputs/Text'
|
||||||
|
import { MarkRenderSignature } from './MarkRender/Signature'
|
||||||
|
|
||||||
|
export const MARK_TYPE_CONFIG: MarkConfigs = {
|
||||||
|
[MarkType.TEXT]: {
|
||||||
|
input: MarkInputText,
|
||||||
|
render: ({ value }) => <>{value}</>
|
||||||
|
},
|
||||||
|
[MarkType.SIGNATURE]: {
|
||||||
|
input: MarkInputSignature,
|
||||||
|
render: MarkRenderSignature
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,8 @@ import {
|
|||||||
CreateSignatureEventContent,
|
CreateSignatureEventContent,
|
||||||
DocSignatureEvent,
|
DocSignatureEvent,
|
||||||
Meta,
|
Meta,
|
||||||
SignedEventContent
|
SignedEventContent,
|
||||||
|
OpenTimestamp
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { Mark } from '../types/mark'
|
import { Mark } from '../types/mark'
|
||||||
import {
|
import {
|
||||||
@ -58,6 +59,8 @@ export interface FlatMeta
|
|||||||
signersStatus: {
|
signersStatus: {
|
||||||
[signer: `npub1${string}`]: SignStatus
|
[signer: `npub1${string}`]: SignStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timestamps?: OpenTimestamp[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -162,7 +165,6 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
setEncryptionKey(decrypted)
|
setEncryptionKey(decrypted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temp. map to hold events and signers
|
// Temp. map to hold events and signers
|
||||||
const parsedSignatureEventsMap = new Map<
|
const parsedSignatureEventsMap = new Map<
|
||||||
`npub1${string}`,
|
`npub1${string}`,
|
||||||
@ -276,6 +278,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
createSignature: meta?.createSignature,
|
createSignature: meta?.createSignature,
|
||||||
docSignatures: meta?.docSignatures,
|
docSignatures: meta?.docSignatures,
|
||||||
keys: meta?.keys,
|
keys: meta?.keys,
|
||||||
|
timestamps: meta?.timestamps,
|
||||||
isValid,
|
isValid,
|
||||||
kind,
|
kind,
|
||||||
tags,
|
tags,
|
||||||
|
@ -19,8 +19,10 @@ import {
|
|||||||
CreateSignatureEventContent,
|
CreateSignatureEventContent,
|
||||||
Meta,
|
Meta,
|
||||||
ProfileMetadata,
|
ProfileMetadata,
|
||||||
|
SignedEvent,
|
||||||
User,
|
User,
|
||||||
UserRole
|
UserRole,
|
||||||
|
KeyboardCode
|
||||||
} from '../../types'
|
} from '../../types'
|
||||||
import {
|
import {
|
||||||
encryptArrayBuffer,
|
encryptArrayBuffer,
|
||||||
@ -62,6 +64,7 @@ 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 _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
|
|
||||||
export const CreatePage = () => {
|
export const CreatePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -85,7 +88,10 @@ export const CreatePage = () => {
|
|||||||
|
|
||||||
const [userInput, setUserInput] = useState('')
|
const [userInput, setUserInput] = useState('')
|
||||||
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
|
if (
|
||||||
|
event.code === KeyboardCode.Enter ||
|
||||||
|
event.code === KeyboardCode.NumpadEnter
|
||||||
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
handleAddUser()
|
handleAddUser()
|
||||||
}
|
}
|
||||||
@ -642,6 +648,11 @@ export const CreatePage = () => {
|
|||||||
return receivers.map((receiver) => sendNotification(receiver, meta))
|
return receivers.map((receiver) => sendNotification(receiver, meta))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractNostrId = (stringifiedEvent: string): string => {
|
||||||
|
const e = JSON.parse(stringifiedEvent) as SignedEvent
|
||||||
|
return e.id
|
||||||
|
}
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
try {
|
try {
|
||||||
if (!validateInputs()) return
|
if (!validateInputs()) return
|
||||||
@ -691,6 +702,12 @@ export const CreatePage = () => {
|
|||||||
const keys = await generateKeys(pubkeys, encryptionKey)
|
const keys = await generateKeys(pubkeys, encryptionKey)
|
||||||
if (!keys) return
|
if (!keys) return
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||||
|
|
||||||
|
const timestamp = await generateTimestamp(
|
||||||
|
extractNostrId(createSignature)
|
||||||
|
)
|
||||||
|
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
createSignature,
|
createSignature,
|
||||||
keys,
|
keys,
|
||||||
@ -698,6 +715,10 @@ export const CreatePage = () => {
|
|||||||
docSignatures: {}
|
docSignatures: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timestamp) {
|
||||||
|
meta.timestamps = [timestamp]
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
@ -9,6 +9,7 @@ import { toast } from 'react-toastify'
|
|||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { AuthController } from '../../controllers'
|
import { AuthController } from '../../controllers'
|
||||||
import { updateKeyPair, updateLoginMethod } from '../../store/actions'
|
import { updateKeyPair, updateLoginMethod } from '../../store/actions'
|
||||||
|
import { KeyboardCode } from '../../types'
|
||||||
import { LoginMethod } from '../../store/auth/types'
|
import { LoginMethod } from '../../store/auth/types'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
|
||||||
@ -52,7 +53,10 @@ export const Nostr = () => {
|
|||||||
* Call login function when enter is pressed
|
* Call login function when enter is pressed
|
||||||
*/
|
*/
|
||||||
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
|
if (
|
||||||
|
event.code === KeyboardCode.Enter ||
|
||||||
|
event.code === KeyboardCode.NumpadEnter
|
||||||
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
login()
|
login()
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,7 @@ import {
|
|||||||
SigitFile
|
SigitFile
|
||||||
} 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'
|
||||||
|
|
||||||
enum SignedStatus {
|
enum SignedStatus {
|
||||||
Fully_Signed,
|
Fully_Signed,
|
||||||
@ -566,6 +567,14 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
const updatedMeta = updateMetaSignatures(meta, signedEvent)
|
const updatedMeta = updateMetaSignatures(meta, signedEvent)
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||||
|
|
||||||
|
const timestamp = await generateTimestamp(signedEvent.id)
|
||||||
|
if (timestamp) {
|
||||||
|
updatedMeta.timestamps = [...(updatedMeta.timestamps || []), timestamp]
|
||||||
|
updatedMeta.modifiedAt = unixNow()
|
||||||
|
}
|
||||||
|
|
||||||
if (await isOnline()) {
|
if (await isOnline()) {
|
||||||
await handleOnlineFlow(updatedMeta)
|
await handleOnlineFlow(updatedMeta)
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,7 +5,13 @@ import { useEffect, useRef, useState } from 'react'
|
|||||||
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 { DocSignatureEvent, Meta } from '../../types'
|
import {
|
||||||
|
DocSignatureEvent,
|
||||||
|
Meta,
|
||||||
|
SignedEvent,
|
||||||
|
OpenTimestamp,
|
||||||
|
OpenTimestampUpgradeVerifyResponse
|
||||||
|
} from '../../types'
|
||||||
import {
|
import {
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
getHash,
|
getHash,
|
||||||
@ -14,7 +20,10 @@ import {
|
|||||||
parseJson,
|
parseJson,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
getCurrentUserFiles
|
getCurrentUserFiles,
|
||||||
|
updateUsersAppData,
|
||||||
|
npubToHex,
|
||||||
|
sendNotification
|
||||||
} 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'
|
||||||
@ -44,6 +53,9 @@ import {
|
|||||||
faFile,
|
faFile,
|
||||||
faFileDownload
|
faFileDownload
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { MARK_TYPE_CONFIG } from '../../components/getMarkComponents.tsx'
|
||||||
|
|
||||||
interface PdfViewProps {
|
interface PdfViewProps {
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
@ -103,6 +115,8 @@ const SlimPdfView = ({
|
|||||||
alt={`page ${i} of ${file.name}`}
|
alt={`page ${i} of ${file.name}`}
|
||||||
/>
|
/>
|
||||||
{marks.map((m) => {
|
{marks.map((m) => {
|
||||||
|
const { render: MarkRenderComponent } =
|
||||||
|
MARK_TYPE_CONFIG[m.type] || {}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`file-mark ${styles.mark}`}
|
className={`file-mark ${styles.mark}`}
|
||||||
@ -118,7 +132,9 @@ const SlimPdfView = ({
|
|||||||
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>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -194,7 +210,8 @@ export const VerifyPage = () => {
|
|||||||
signers,
|
signers,
|
||||||
viewers,
|
viewers,
|
||||||
fileHashes,
|
fileHashes,
|
||||||
parsedSignatureEvents
|
parsedSignatureEvents,
|
||||||
|
timestamps
|
||||||
} = useSigitMeta(meta)
|
} = useSigitMeta(meta)
|
||||||
|
|
||||||
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
||||||
@ -204,6 +221,16 @@ export const VerifyPage = () => {
|
|||||||
[key: string]: string | null
|
[key: string]: string | null
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
|
const signTimestampEvent = async (signerContent: {
|
||||||
|
timestamps: OpenTimestamp[]
|
||||||
|
}): Promise<SignedEvent | null> => {
|
||||||
|
return await signEventForMetaFile(
|
||||||
|
JSON.stringify(signerContent),
|
||||||
|
nostrController,
|
||||||
|
setIsLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Object.entries(files).length > 0) {
|
if (Object.entries(files).length > 0) {
|
||||||
const tmp = getCurrentUserFiles(files, currentFileHashes, fileHashes)
|
const tmp = getCurrentUserFiles(files, currentFileHashes, fileHashes)
|
||||||
@ -211,6 +238,147 @@ export const VerifyPage = () => {
|
|||||||
}
|
}
|
||||||
}, [currentFileHashes, fileHashes, files])
|
}, [currentFileHashes, fileHashes, files])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
timestamps &&
|
||||||
|
timestamps.length > 0 &&
|
||||||
|
usersPubkey &&
|
||||||
|
submittedBy &&
|
||||||
|
parsedSignatureEvents
|
||||||
|
) {
|
||||||
|
if (timestamps.every((t) => !!t.verification)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const upgradeT = async (timestamps: OpenTimestamp[]) => {
|
||||||
|
try {
|
||||||
|
setLoadingSpinnerDesc('Upgrading your timestamps.')
|
||||||
|
|
||||||
|
const findCreatorTimestamp = (timestamps: OpenTimestamp[]) => {
|
||||||
|
if (usersPubkey === submittedBy) {
|
||||||
|
return timestamps[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const findSignerTimestamp = (timestamps: OpenTimestamp[]) => {
|
||||||
|
const parsedEvent = parsedSignatureEvents[hexToNpub(usersPubkey)]
|
||||||
|
if (parsedEvent?.id) {
|
||||||
|
return timestamps.find((t) => t.nostrId === parsedEvent.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if timestamp verification has been achieved for the first time.
|
||||||
|
* Note that the upgrade flag is separate from verification. It is possible for a timestamp
|
||||||
|
* to not be upgraded, but to be verified for the first time.
|
||||||
|
* @param upgradedTimestamp
|
||||||
|
* @param timestamps
|
||||||
|
*/
|
||||||
|
const isNewlyVerified = (
|
||||||
|
upgradedTimestamp: OpenTimestampUpgradeVerifyResponse,
|
||||||
|
timestamps: OpenTimestamp[]
|
||||||
|
) => {
|
||||||
|
if (!upgradedTimestamp.verified) return false
|
||||||
|
const oldT = timestamps.find(
|
||||||
|
(t) => t.nostrId === upgradedTimestamp.timestamp.nostrId
|
||||||
|
)
|
||||||
|
if (!oldT) return false
|
||||||
|
if (!oldT.verification && upgradedTimestamp.verified) return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const userTimestamps: OpenTimestamp[] = []
|
||||||
|
|
||||||
|
const creatorTimestamp = findCreatorTimestamp(timestamps)
|
||||||
|
if (creatorTimestamp) {
|
||||||
|
userTimestamps.push(creatorTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const signerTimestamp = findSignerTimestamp(timestamps)
|
||||||
|
if (signerTimestamp) {
|
||||||
|
userTimestamps.push(signerTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userTimestamps.every((t) => !!t.verification)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const upgradedUserTimestamps = await Promise.all(
|
||||||
|
userTimestamps.map(upgradeAndVerifyTimestamp)
|
||||||
|
)
|
||||||
|
|
||||||
|
const upgradedTimestamps = upgradedUserTimestamps
|
||||||
|
.filter((t) => t.upgraded || isNewlyVerified(t, userTimestamps))
|
||||||
|
.map((t) => {
|
||||||
|
const timestamp: OpenTimestamp = { ...t.timestamp }
|
||||||
|
if (t.verified) {
|
||||||
|
timestamp.verification = t.verification
|
||||||
|
}
|
||||||
|
return timestamp
|
||||||
|
})
|
||||||
|
|
||||||
|
if (upgradedTimestamps.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Signing a timestamp upgrade event.')
|
||||||
|
|
||||||
|
const signedEvent = await signTimestampEvent({
|
||||||
|
timestamps: upgradedTimestamps
|
||||||
|
})
|
||||||
|
if (!signedEvent) return
|
||||||
|
|
||||||
|
const finalTimestamps = timestamps.map((t) => {
|
||||||
|
const upgraded = upgradedTimestamps.find(
|
||||||
|
(tu) => tu.nostrId === t.nostrId
|
||||||
|
)
|
||||||
|
if (upgraded) {
|
||||||
|
return {
|
||||||
|
...upgraded,
|
||||||
|
signature: JSON.stringify(signedEvent, null, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedMeta = _.cloneDeep(meta)
|
||||||
|
updatedMeta.timestamps = [...finalTimestamps]
|
||||||
|
updatedMeta.modifiedAt = unixNow()
|
||||||
|
|
||||||
|
const updatedEvent = await updateUsersAppData(updatedMeta)
|
||||||
|
if (!updatedEvent) return
|
||||||
|
|
||||||
|
const userSet = new Set<`npub1${string}`>()
|
||||||
|
signers.forEach((signer) => {
|
||||||
|
if (signer !== usersPubkey) {
|
||||||
|
userSet.add(signer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
viewers.forEach((viewer) => {
|
||||||
|
userSet.add(viewer)
|
||||||
|
})
|
||||||
|
|
||||||
|
const users = Array.from(userSet)
|
||||||
|
const promises = users.map((user) =>
|
||||||
|
sendNotification(npubToHex(user)!, updatedMeta)
|
||||||
|
)
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
|
toast.success('Timestamp updates have been sent successfully.')
|
||||||
|
|
||||||
|
setMeta(meta)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toast.error(
|
||||||
|
'There was an error upgrading or verifying your timestamps!'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
upgradeT(timestamps)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [timestamps, submittedBy, parsedSignatureEvents])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (metaInNavState && encryptionKey) {
|
if (metaInNavState && encryptionKey) {
|
||||||
const processSigit = async () => {
|
const processSigit = async () => {
|
||||||
|
@ -18,6 +18,7 @@ export interface Meta {
|
|||||||
docSignatures: { [key: `npub1${string}`]: string }
|
docSignatures: { [key: `npub1${string}`]: string }
|
||||||
exportSignature?: string
|
exportSignature?: string
|
||||||
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
|
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
|
||||||
|
timestamps?: OpenTimestamp[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateSignatureEventContent {
|
export interface CreateSignatureEventContent {
|
||||||
@ -39,6 +40,25 @@ export interface Sigit {
|
|||||||
meta: Meta
|
meta: Meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenTimestamp {
|
||||||
|
nostrId: string
|
||||||
|
value: string
|
||||||
|
verification?: OpenTimestampVerification
|
||||||
|
signature?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenTimestampVerification {
|
||||||
|
height: number
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenTimestampUpgradeVerifyResponse {
|
||||||
|
timestamp: OpenTimestamp
|
||||||
|
upgraded: boolean
|
||||||
|
verified?: boolean
|
||||||
|
verification?: OpenTimestampVerification
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserAppData {
|
export interface UserAppData {
|
||||||
/**
|
/**
|
||||||
* Key will be id of create signature
|
* Key will be id of create signature
|
||||||
|
5
src/types/event.ts
Normal file
5
src/types/event.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum KeyboardCode {
|
||||||
|
Escape = 'Escape',
|
||||||
|
Enter = 'Enter',
|
||||||
|
NumpadEnter = 'NumpadEnter'
|
||||||
|
}
|
@ -4,3 +4,4 @@ export * from './nostr'
|
|||||||
export * from './profile'
|
export * from './profile'
|
||||||
export * from './relay'
|
export * from './relay'
|
||||||
export * from './zip'
|
export * from './zip'
|
||||||
|
export * from './event'
|
||||||
|
@ -28,3 +28,24 @@ export interface MarkRect {
|
|||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MarkInputProps {
|
||||||
|
value: string
|
||||||
|
handler: (value: string) => void
|
||||||
|
placeholder?: string
|
||||||
|
userMark?: CurrentUserMark
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkRenderProps {
|
||||||
|
value?: string
|
||||||
|
mark: Mark
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkConfig {
|
||||||
|
input: React.FC<MarkInputProps>
|
||||||
|
render?: React.FC<MarkRenderProps>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MarkConfigs = {
|
||||||
|
[key in MarkType]?: MarkConfig
|
||||||
|
}
|
||||||
|
38
src/types/opentimestamps.d.ts
vendored
Normal file
38
src/types/opentimestamps.d.ts
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
interface OpenTimestamps {
|
||||||
|
// Create a detached timestamp file from a buffer or file hash
|
||||||
|
DetachedTimestampFile: {
|
||||||
|
fromHash(op: any, hash: Uint8Array): any
|
||||||
|
fromBytes(op: any, buffer: Uint8Array): any
|
||||||
|
deserialize(buffer: any): any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stamp the provided timestamp file and return a Promise
|
||||||
|
stamp(file: any): Promise<void>
|
||||||
|
|
||||||
|
// Verify the provided timestamp proof file
|
||||||
|
verify(
|
||||||
|
ots: string,
|
||||||
|
file: string
|
||||||
|
): Promise<TimestampVerficiationResponse | Record<string, never>>
|
||||||
|
|
||||||
|
// Other utilities or operations (like OpSHA256, serialization)
|
||||||
|
Ops: {
|
||||||
|
OpSHA256: any
|
||||||
|
OpSHA1?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
Context: {
|
||||||
|
StreamSerialization: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load a timestamp file from a buffer
|
||||||
|
deserialize(bytes: Uint8Array): any
|
||||||
|
|
||||||
|
// Other potential methods based on repo functions
|
||||||
|
upgrade(file: any): Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimestampVerficiationResponse {
|
||||||
|
bitcoin: { timestamp: number; height: number }
|
||||||
|
}
|
1
src/types/system/index.d.ts
vendored
1
src/types/system/index.d.ts
vendored
@ -3,5 +3,6 @@ import type { WindowNostr } from 'nostr-tools/nip07'
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
nostr?: WindowNostr
|
nostr?: WindowNostr
|
||||||
|
OpenTimestamps: OpenTimestamps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CurrentUserMark, Mark } from '../types/mark.ts'
|
import { CurrentUserMark, Mark, MarkLocation } from '../types/mark.ts'
|
||||||
import { hexToNpub } from './nostr.ts'
|
import { hexToNpub } from './nostr.ts'
|
||||||
import { Meta, SignedEventContent } from '../types'
|
import { Meta, SignedEventContent } from '../types'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
@ -24,6 +24,7 @@ import {
|
|||||||
faStamp,
|
faStamp,
|
||||||
faTableCellsLarge
|
faTableCellsLarge
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { Config, optimize } from 'svgo'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes in an array of Marks already filtered by User.
|
* Takes in an array of Marks already filtered by User.
|
||||||
@ -158,6 +159,11 @@ export const DEFAULT_TOOLBOX: DrawTool[] = [
|
|||||||
icon: faT,
|
icon: faT,
|
||||||
label: 'Text'
|
label: 'Text'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.SIGNATURE,
|
||||||
|
icon: faSignature,
|
||||||
|
label: 'Signature'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.FULLNAME,
|
identifier: MarkType.FULLNAME,
|
||||||
icon: faIdCard,
|
icon: faIdCard,
|
||||||
@ -170,12 +176,6 @@ export const DEFAULT_TOOLBOX: DrawTool[] = [
|
|||||||
label: 'Job Title',
|
label: 'Job Title',
|
||||||
isComingSoon: true
|
isComingSoon: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
identifier: MarkType.SIGNATURE,
|
|
||||||
icon: faSignature,
|
|
||||||
label: 'Signature',
|
|
||||||
isComingSoon: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
identifier: MarkType.DATETIME,
|
identifier: MarkType.DATETIME,
|
||||||
icon: faClock,
|
icon: faClock,
|
||||||
@ -266,6 +266,24 @@ export const getToolboxLabelByMarkType = (markType: MarkType) => {
|
|||||||
return DEFAULT_TOOLBOX.find((t) => t.identifier === markType)?.label
|
return DEFAULT_TOOLBOX.find((t) => t.identifier === markType)?.label
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const optimizeSVG = (location: MarkLocation, paths: string[]) => {
|
||||||
|
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="${location.width}" height="${location.height}">${paths.map((path) => `<path d="${path}" stroke="black" fill="none" />`).join('')}</svg>`
|
||||||
|
const optimizedSVG = optimize(svgContent, {
|
||||||
|
multipass: true, // Optimize multiple times if needed
|
||||||
|
floatPrecision: 2 // Adjust precision
|
||||||
|
} as Config)
|
||||||
|
|
||||||
|
return optimizedSVG.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOptimizedPaths = (svgString: string) => {
|
||||||
|
const regex = / d="([^"]*)"/g
|
||||||
|
const matches = [...svgString.matchAll(regex)]
|
||||||
|
const pathValues = matches.map((match) => match[1])
|
||||||
|
|
||||||
|
return pathValues
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getCurrentUserMarks,
|
getCurrentUserMarks,
|
||||||
filterMarksByPubkey,
|
filterMarksByPubkey,
|
||||||
|
119
src/utils/opentimestamps.ts
Normal file
119
src/utils/opentimestamps.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { OpenTimestamp, OpenTimestampUpgradeVerifyResponse } from '../types'
|
||||||
|
import { retry } from './retry.ts'
|
||||||
|
import { bytesToHex } from '@noble/hashes/utils'
|
||||||
|
import { utf8Encoder } from 'nostr-tools/utils'
|
||||||
|
import { hexStringToUint8Array } from './string.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a timestamp for the provided nostr event ID.
|
||||||
|
* @returns Timestamp with its value and the nostr event ID.
|
||||||
|
*/
|
||||||
|
export const generateTimestamp = async (
|
||||||
|
nostrId: string
|
||||||
|
): Promise<OpenTimestamp | undefined> => {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
value: await retry(() => timestamp(nostrId)),
|
||||||
|
nostrId: nostrId
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to upgrade (i.e. add Bitcoin blockchain attestations) and verify the provided timestamp.
|
||||||
|
* Returns the same timestamp, alongside additional information required to decide if any further
|
||||||
|
* timestamp updates are required.
|
||||||
|
* @param timestamp
|
||||||
|
*/
|
||||||
|
export const upgradeAndVerifyTimestamp = async (
|
||||||
|
timestamp: OpenTimestamp
|
||||||
|
): Promise<OpenTimestampUpgradeVerifyResponse> => {
|
||||||
|
const upgradedResult = await upgrade(timestamp)
|
||||||
|
return await verify(upgradedResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to upgrade a timestamp. If an upgrade is available,
|
||||||
|
* it will add new data to detachedTimestamp.
|
||||||
|
* The upgraded flag indicates if an upgrade has been performed.
|
||||||
|
* @param t - timestamp
|
||||||
|
*/
|
||||||
|
export const upgrade = async (
|
||||||
|
t: OpenTimestamp
|
||||||
|
): Promise<OpenTimestampUpgradeVerifyResponse> => {
|
||||||
|
const detachedTimestamp =
|
||||||
|
window.OpenTimestamps.DetachedTimestampFile.deserialize(
|
||||||
|
hexStringToUint8Array(t.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const changed: boolean =
|
||||||
|
await window.OpenTimestamps.upgrade(detachedTimestamp)
|
||||||
|
if (changed) {
|
||||||
|
const updated = detachedTimestamp.serializeToBytes()
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
...t,
|
||||||
|
timestamp: bytesToHex(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: value,
|
||||||
|
upgraded: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
timestamp: t,
|
||||||
|
upgraded: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to verify a timestamp. If verification is available,
|
||||||
|
* it will be included in the returned object.
|
||||||
|
* @param t - timestamp
|
||||||
|
*/
|
||||||
|
export const verify = async (
|
||||||
|
t: OpenTimestampUpgradeVerifyResponse
|
||||||
|
): Promise<OpenTimestampUpgradeVerifyResponse> => {
|
||||||
|
const detachedNostrId = window.OpenTimestamps.DetachedTimestampFile.fromBytes(
|
||||||
|
new window.OpenTimestamps.Ops.OpSHA256(),
|
||||||
|
utf8Encoder.encode(t.timestamp.nostrId)
|
||||||
|
)
|
||||||
|
|
||||||
|
const detachedTimestamp =
|
||||||
|
window.OpenTimestamps.DetachedTimestampFile.deserialize(
|
||||||
|
hexStringToUint8Array(t.timestamp.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await window.OpenTimestamps.verify(
|
||||||
|
detachedTimestamp,
|
||||||
|
detachedNostrId
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...t,
|
||||||
|
verified: !!res.bitcoin,
|
||||||
|
verification: res?.bitcoin || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps a nostrId.
|
||||||
|
* @param nostrId
|
||||||
|
*/
|
||||||
|
const timestamp = async (nostrId: string): Promise<string> => {
|
||||||
|
const detachedTimestamp =
|
||||||
|
window.OpenTimestamps.DetachedTimestampFile.fromBytes(
|
||||||
|
new window.OpenTimestamps.Ops.OpSHA256(),
|
||||||
|
utf8Encoder.encode(nostrId)
|
||||||
|
)
|
||||||
|
|
||||||
|
await window.OpenTimestamps.stamp(detachedTimestamp)
|
||||||
|
const ctx = new window.OpenTimestamps.Context.StreamSerialization()
|
||||||
|
detachedTimestamp.serialize(ctx)
|
||||||
|
const timestampBytes = ctx.getOutput()
|
||||||
|
return bytesToHex(timestampBytes)
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { PdfPage } from '../types/drawing.ts'
|
import { MarkType, PdfPage } from '../types/drawing.ts'
|
||||||
import { 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'
|
||||||
@ -132,9 +132,17 @@ export const addMarks = async (
|
|||||||
|
|
||||||
for (let i = 0; i < pages.length; i++) {
|
for (let i = 0; i < pages.length; i++) {
|
||||||
if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
|
if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
|
||||||
marksPerPage[i]?.forEach((mark) =>
|
marksPerPage[i]?.forEach((mark) => {
|
||||||
drawMarkText(mark, pages[i], robotoFont)
|
switch (mark.type) {
|
||||||
)
|
case MarkType.SIGNATURE:
|
||||||
|
drawSignatureText(mark, pages[i])
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
drawMarkText(mark, pages[i], robotoFont)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,3 +253,19 @@ async function embedFont(pdf: PDFDocument) {
|
|||||||
const embeddedFont = await pdf.embedFont(fontBytes)
|
const embeddedFont = await pdf.embedFont(fontBytes)
|
||||||
return embeddedFont
|
return embeddedFont
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const drawSignatureText = (mark: Mark, page: PDFPage) => {
|
||||||
|
const { location } = mark
|
||||||
|
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)) {
|
||||||
|
const paths = JSON.parse(mark.value!)
|
||||||
|
paths.forEach((d: string) => {
|
||||||
|
page.drawSvgPath(d, { x, y })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
25
src/utils/retry.ts
Normal file
25
src/utils/retry.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export const retryAll = async <T>(
|
||||||
|
promises: (() => Promise<T>)[],
|
||||||
|
retries: number = 3,
|
||||||
|
delay: number = 1000
|
||||||
|
) => {
|
||||||
|
const wrappedPromises = promises.map((fn) => retry(fn, retries, delay))
|
||||||
|
return Promise.allSettled(wrappedPromises)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const retry = async <T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
retries: number = 3,
|
||||||
|
delay: number = 1000
|
||||||
|
): Promise<T> => {
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} catch (err) {
|
||||||
|
if (retries === 0) {
|
||||||
|
return Promise.reject(err)
|
||||||
|
}
|
||||||
|
return new Promise((resolve) =>
|
||||||
|
setTimeout(() => resolve(retry(fn, retries - 1)), delay)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -119,3 +119,15 @@ export const settleAllFullfilfedPromises = async <Item, FulfilledItem = Item>(
|
|||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isPromiseFulfilled = <T>(
|
||||||
|
result: PromiseSettledResult<T>
|
||||||
|
): result is PromiseFulfilledResult<T> => {
|
||||||
|
return result.status === 'fulfilled'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isPromiseRejected = <T>(
|
||||||
|
result: PromiseSettledResult<T>
|
||||||
|
): result is PromiseRejectedResult => {
|
||||||
|
return result.status === 'rejected'
|
||||||
|
}
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||||
|
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tsconfigPaths()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tsconfigPaths(),
|
||||||
|
nodePolyfills({
|
||||||
|
include: ['os']
|
||||||
|
})
|
||||||
|
],
|
||||||
build: {
|
build: {
|
||||||
target: 'ES2022'
|
target: 'ES2022'
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user