chore(git): merge
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 39s
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 39s
This commit is contained in:
commit
a2b7423c8b
13
package-lock.json
generated
13
package-lock.json
generated
@ -49,7 +49,8 @@
|
|||||||
"react-toastify": "10.0.4",
|
"react-toastify": "10.0.4",
|
||||||
"redux": "5.0.1",
|
"redux": "5.0.1",
|
||||||
"signature_pad": "^5.0.4",
|
"signature_pad": "^5.0.4",
|
||||||
"tseep": "1.2.1"
|
"tseep": "1.2.1",
|
||||||
|
"use-immer": "^0.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
@ -8591,6 +8592,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/use-immer": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-RNAqi3GqsWJ4bcCd4LMBgdzvPmTABam24DUaFiKfX9s3MSorNRz9RDZYJkllJoMHUxVLMDetwAuCDeyWNrp1yA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"immer": ">=8.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
"node_modules/use-sync-external-store": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||||
|
@ -59,7 +59,8 @@
|
|||||||
"react-toastify": "10.0.4",
|
"react-toastify": "10.0.4",
|
||||||
"redux": "5.0.1",
|
"redux": "5.0.1",
|
||||||
"signature_pad": "^5.0.4",
|
"signature_pad": "^5.0.4",
|
||||||
"tseep": "1.2.1"
|
"tseep": "1.2.1",
|
||||||
|
"use-immer": "^0.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
@ -8,23 +8,24 @@ import {
|
|||||||
Select
|
Select
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { ProfileMetadata, User, UserRole, KeyboardCode } from '../../types'
|
import { ProfileMetadata, User, UserRole, KeyboardCode } from '../../types'
|
||||||
import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
|
import { MouseState, DrawnField, DrawTool } from '../../types/drawing'
|
||||||
import { hexToNpub, npubToHex, getProfileUsername } from '../../utils'
|
import { hexToNpub, npubToHex, getProfileUsername } from '../../utils'
|
||||||
import { SigitFile } from '../../utils/file'
|
import { SigitFile } from '../../utils/file'
|
||||||
import { getToolboxLabelByMarkType } from '../../utils/mark'
|
import { getToolboxLabelByMarkType } from '../../utils/mark'
|
||||||
import { FileDivider } from '../FileDivider'
|
|
||||||
import { ExtensionFileBox } from '../ExtensionFileBox'
|
|
||||||
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf'
|
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf'
|
||||||
import { useScale } from '../../hooks/useScale'
|
import { useScale } from '../../hooks/useScale'
|
||||||
import { AvatarIconButton } from '../UserAvatarIconButton'
|
import { AvatarIconButton } from '../UserAvatarIconButton'
|
||||||
import { UserAvatar } from '../UserAvatar'
|
import { UserAvatar } from '../UserAvatar'
|
||||||
import _ from 'lodash'
|
import { Updater } from 'use-immer'
|
||||||
|
import { FileItem } from './internal/FileItem'
|
||||||
|
import { FileDivider } from '../FileDivider'
|
||||||
|
import { Counterpart } from './internal/Counterpart'
|
||||||
|
|
||||||
const MINIMUM_RECT_SIZE = {
|
const MINIMUM_RECT_SIZE = {
|
||||||
width: 21,
|
width: 10,
|
||||||
height: 21
|
height: 10
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const DEFAULT_START_SIZE = {
|
const DEFAULT_START_SIZE = {
|
||||||
@ -36,16 +37,25 @@ interface HideSignersForDrawnField {
|
|||||||
[key: number]: boolean
|
[key: number]: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
type PageIndexer = [file: number, page: number]
|
||||||
|
type FieldIndexer = [...PageIndexer, field: number]
|
||||||
|
|
||||||
|
interface DrawPdfFieldsProps {
|
||||||
users: User[]
|
users: User[]
|
||||||
metadata: { [key: string]: ProfileMetadata }
|
metadata: { [key: string]: ProfileMetadata }
|
||||||
sigitFiles: SigitFile[]
|
sigitFiles: SigitFile[]
|
||||||
setSigitFiles: React.Dispatch<React.SetStateAction<SigitFile[]>>
|
updateSigitFiles: Updater<SigitFile[]>
|
||||||
selectedTool?: DrawTool
|
selectedTool?: DrawTool
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DrawPDFFields = (props: Props) => {
|
export const DrawPDFFields = ({
|
||||||
const { selectedTool, sigitFiles, setSigitFiles, users } = props
|
selectedTool,
|
||||||
|
metadata,
|
||||||
|
sigitFiles,
|
||||||
|
updateSigitFiles,
|
||||||
|
users
|
||||||
|
}: DrawPdfFieldsProps) => {
|
||||||
|
const { to, from } = useScale()
|
||||||
|
|
||||||
const signers = users.filter((u) => u.role === UserRole.signer)
|
const signers = users.filter((u) => u.role === UserRole.signer)
|
||||||
const defaultSignerNpub = signers.length ? hexToNpub(signers[0].pubkey) : ''
|
const defaultSignerNpub = signers.length ? hexToNpub(signers[0].pubkey) : ''
|
||||||
@ -58,158 +68,124 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* @param pubkeys
|
* @param pubkeys
|
||||||
* @returns available pubkey or empty string
|
* @returns available pubkey or empty string
|
||||||
*/
|
*/
|
||||||
const getAvailableSigner = (...pubkeys: string[]) => {
|
const getAvailableSigner = useCallback(
|
||||||
|
(...pubkeys: string[]) => {
|
||||||
const availableSigner: string | undefined = pubkeys.find((pubkey) =>
|
const availableSigner: string | undefined = pubkeys.find((pubkey) =>
|
||||||
signers.some((s) => s.pubkey === npubToHex(pubkey))
|
signers.some((s) => s.pubkey === npubToHex(pubkey))
|
||||||
)
|
)
|
||||||
return availableSigner || ''
|
return availableSigner || ''
|
||||||
|
},
|
||||||
|
[signers]
|
||||||
|
)
|
||||||
|
|
||||||
|
const [mouseState, setMouseState] = useState<MouseState>({})
|
||||||
|
const [indexer, setIndexer] = useState<PageIndexer | FieldIndexer>()
|
||||||
|
const [field, setField] = useState<
|
||||||
|
DrawnField & {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
}
|
}
|
||||||
|
>()
|
||||||
const { to, from } = useScale()
|
const [lastIndexer, setLastIndexer] = useState<FieldIndexer>()
|
||||||
|
|
||||||
const [mouseState, setMouseState] = useState<MouseState>({
|
|
||||||
clicked: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const [activeDrawnField, setActiveDrawnField] = useState<{
|
|
||||||
fileIndex: number
|
|
||||||
pageIndex: number
|
|
||||||
drawnFieldIndex: number
|
|
||||||
}>()
|
|
||||||
const isActiveDrawnField = (
|
const isActiveDrawnField = (
|
||||||
fileIndex: number,
|
fileIndex: number,
|
||||||
pageIndex: number,
|
pageIndex: number,
|
||||||
drawnFieldIndex: number
|
drawnFieldIndex: number
|
||||||
) =>
|
) =>
|
||||||
activeDrawnField?.fileIndex === fileIndex &&
|
lastIndexer &&
|
||||||
activeDrawnField?.pageIndex === pageIndex &&
|
lastIndexer[0] === fileIndex &&
|
||||||
activeDrawnField?.drawnFieldIndex === drawnFieldIndex
|
lastIndexer[1] === pageIndex &&
|
||||||
|
lastIndexer[2] === drawnFieldIndex
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drawing events
|
* Gets the pointer coordinates relative to a element in the `event` param
|
||||||
|
* @param event PointerEvent
|
||||||
|
* @param customTarget coordinates relative to this element, if not provided
|
||||||
|
* event.target will be used
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
const getPointerCoordinates = (
|
||||||
window.addEventListener('pointerup', handlePointerUp)
|
event: React.PointerEvent,
|
||||||
window.addEventListener('pointercancel', handlePointerUp)
|
customTarget?: HTMLElement | null
|
||||||
|
) => {
|
||||||
|
const target = customTarget ? customTarget : event.currentTarget
|
||||||
|
const rect = target.getBoundingClientRect()
|
||||||
|
|
||||||
return () => {
|
// Clamp X Y within the target
|
||||||
window.removeEventListener('pointerup', handlePointerUp)
|
const x = Math.max(0, Math.min(event.clientX, rect.right) - rect.left) //x position within the element.
|
||||||
window.removeEventListener('pointercancel', handlePointerUp)
|
const y = Math.max(0, Math.min(event.clientY, rect.bottom) - rect.top) //y position within the element.
|
||||||
|
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
rect
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const refreshPdfFiles = () => {
|
|
||||||
setSigitFiles([...sigitFiles])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired only on when left (primary pointer interaction) clicking page image
|
* Fired only on when left (primary pointer interaction) clicking page image
|
||||||
* Creates new drawnElement and pushes in the array
|
* Creates new drawnElement
|
||||||
* It is re rendered and visible right away
|
|
||||||
*
|
*
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param page PdfPage where press happened
|
* @param pageIndexer File and page index
|
||||||
|
* @param pageWidth pdf value used to scale pointer coordinates
|
||||||
*/
|
*/
|
||||||
const handlePointerDown = (
|
const handlePointerDown = useCallback(
|
||||||
|
(
|
||||||
event: React.PointerEvent,
|
event: React.PointerEvent,
|
||||||
page: PdfPage,
|
pageIndexer: PageIndexer,
|
||||||
fileIndex: number,
|
pageWidth: number
|
||||||
pageIndex: number
|
|
||||||
) => {
|
) => {
|
||||||
// Proceed only if left click
|
// Proceed only if left click
|
||||||
if (event.button !== 0) return
|
if (event.button !== 0) return
|
||||||
|
if (!selectedTool) return
|
||||||
if (!selectedTool) {
|
event.currentTarget.setPointerCapture(event.pointerId)
|
||||||
return
|
const counterpart = getAvailableSigner(lastSigner, defaultSignerNpub)
|
||||||
}
|
|
||||||
|
|
||||||
const { x, y } = getPointerCoordinates(event)
|
const { x, y } = getPointerCoordinates(event)
|
||||||
|
|
||||||
const newField: DrawnField = {
|
setIndexer(pageIndexer)
|
||||||
left: to(page.width, x),
|
setField({
|
||||||
top: to(page.width, y),
|
x: to(pageWidth, x),
|
||||||
|
y: to(pageWidth, y),
|
||||||
|
left: to(pageWidth, x),
|
||||||
|
top: to(pageWidth, y),
|
||||||
width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width,
|
width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width,
|
||||||
height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height,
|
height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height,
|
||||||
counterpart: getAvailableSigner(lastSigner, defaultSignerNpub),
|
type: selectedTool.identifier,
|
||||||
type: selectedTool.identifier
|
counterpart
|
||||||
}
|
|
||||||
|
|
||||||
page.drawnFields.push(newField)
|
|
||||||
|
|
||||||
setActiveDrawnField({
|
|
||||||
fileIndex,
|
|
||||||
pageIndex,
|
|
||||||
drawnFieldIndex: page.drawnFields.length - 1
|
|
||||||
})
|
})
|
||||||
setMouseState((prev) => {
|
setMouseState({
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
clicked: true
|
clicked: true
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
[defaultSignerNpub, getAvailableSigner, lastSigner, selectedTool, to]
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drawing is finished, resets all the variables used to draw
|
* After {@link handlePointerDown} creates an drawing element, this function
|
||||||
|
* alters the newly created drawing element, resizing it while pointer moves
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
|
* @param pageWidth pdf value used to scale pointer coordinates
|
||||||
*/
|
*/
|
||||||
const handlePointerUp = () => {
|
const handlePointerMove = useCallback(
|
||||||
sigitFiles.forEach((s) => {
|
(event: React.PointerEvent, pageWidth: number) => {
|
||||||
s.pages?.forEach((p) => {
|
|
||||||
// Remove drawn fields below the MINIMUM_RECT_SIZE threshhold
|
|
||||||
p.drawnFields = p.drawnFields.filter(
|
|
||||||
(f) =>
|
|
||||||
!(
|
|
||||||
f.width < MINIMUM_RECT_SIZE.width ||
|
|
||||||
f.height < MINIMUM_RECT_SIZE.height
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
setMouseState((prev) => {
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
clicked: false,
|
|
||||||
dragging: false,
|
|
||||||
resizing: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
refreshPdfFiles()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* After {@link handlePointerDown} create an drawing element, this function gets called on every pixel moved
|
|
||||||
* which alters the newly created drawing element, resizing it while pointer moves
|
|
||||||
* @param event Pointer event
|
|
||||||
* @param page PdfPage where moving is happening
|
|
||||||
*/
|
|
||||||
const handlePointerMove = (event: React.PointerEvent, page: PdfPage) => {
|
|
||||||
if (mouseState.clicked && selectedTool && event.pointerType === 'mouse') {
|
if (mouseState.clicked && selectedTool && event.pointerType === 'mouse') {
|
||||||
const lastElementIndex = page.drawnFields.length - 1
|
|
||||||
|
|
||||||
const lastDrawnField = page.drawnFields[lastElementIndex]
|
|
||||||
|
|
||||||
// Return early if we don't have lastDrawnField
|
|
||||||
// Issue noticed in the console when dragging out of bounds
|
|
||||||
// to the page below (without releaseing mouse click)
|
|
||||||
if (!lastDrawnField) return
|
|
||||||
|
|
||||||
const { x, y } = getPointerCoordinates(event)
|
const { x, y } = getPointerCoordinates(event)
|
||||||
|
const pageX = to(pageWidth, x)
|
||||||
|
const pageY = to(pageWidth, y)
|
||||||
|
|
||||||
const width = to(page.width, x) - lastDrawnField.left
|
// Calculate left, top, width, and height based on direction
|
||||||
const height = to(page.width, y) - lastDrawnField.top
|
setField((prev) => {
|
||||||
|
const left = pageX < prev!.x ? pageX : prev!.x
|
||||||
|
const top = pageY < prev!.y ? pageY : prev!.y
|
||||||
|
|
||||||
lastDrawnField.width = width
|
const width = Math.abs(pageX - prev!.x)
|
||||||
lastDrawnField.height = height
|
const height = Math.abs(pageY - prev!.y)
|
||||||
|
return { ...prev!, left, top, width, height }
|
||||||
const currentDrawnFields = page.drawnFields
|
})
|
||||||
|
|
||||||
currentDrawnFields[lastElementIndex] = lastDrawnField
|
|
||||||
|
|
||||||
refreshPdfFiles()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[mouseState.clicked, selectedTool, to]
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired when event happens on the drawn element which will be moved
|
* Fired when event happens on the drawn element which will be moved
|
||||||
@ -219,22 +195,30 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* y - offsetY
|
* y - offsetY
|
||||||
*
|
*
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param drawnFieldIndex Which we are moving
|
* @param fieldIndexer Which field we are moving
|
||||||
*/
|
*/
|
||||||
const handleDrawnFieldPointerDown = (
|
const handleDrawnFieldPointerDown = (
|
||||||
event: React.PointerEvent,
|
event: React.PointerEvent,
|
||||||
fileIndex: number,
|
fieldIndexer: FieldIndexer
|
||||||
pageIndex: number,
|
|
||||||
drawnFieldIndex: number
|
|
||||||
) => {
|
) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
// Proceed only if left click
|
// Proceed only if left click
|
||||||
if (event.button !== 0) return
|
if (event.button !== 0) return
|
||||||
|
|
||||||
|
event.currentTarget.setPointerCapture(event.pointerId)
|
||||||
const drawingRectangleCoords = getPointerCoordinates(event)
|
const drawingRectangleCoords = getPointerCoordinates(event)
|
||||||
|
const [fileIndex, pageIndex, drawnFieldIndex] = fieldIndexer
|
||||||
|
const page = sigitFiles[fileIndex].pages![pageIndex]
|
||||||
|
const drawnField = page.drawnFields[drawnFieldIndex]
|
||||||
|
|
||||||
setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex })
|
setIndexer(fieldIndexer)
|
||||||
|
setField({
|
||||||
|
...drawnField,
|
||||||
|
x: to(page.width, drawingRectangleCoords.x),
|
||||||
|
y: to(page.width, drawingRectangleCoords.y)
|
||||||
|
})
|
||||||
|
setLastIndexer(fieldIndexer)
|
||||||
setMouseState({
|
setMouseState({
|
||||||
dragging: true,
|
dragging: true,
|
||||||
clicked: false,
|
clicked: false,
|
||||||
@ -244,7 +228,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// make signers dropdown visible
|
// Make signers dropdown visible
|
||||||
setHideSignersForDrawnField((prev) => ({
|
setHideSignersForDrawnField((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[drawnFieldIndex]: false
|
[drawnFieldIndex]: false
|
||||||
@ -254,12 +238,10 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
/**
|
/**
|
||||||
* Moves the drawnElement by the pointer position (pointer can grab anywhere on the drawn element)
|
* Moves the drawnElement by the pointer position (pointer can grab anywhere on the drawn element)
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param drawnField which we are moving
|
* @param pageWidth pdf value used to scale pointer coordinates
|
||||||
* @param pageWidth pdf value which is used to calculate scaled offset
|
|
||||||
*/
|
*/
|
||||||
const handleDrawnFieldPointerMove = (
|
const handleDrawnFieldPointerMove = (
|
||||||
event: React.PointerEvent,
|
event: React.PointerEvent,
|
||||||
drawnField: DrawnField,
|
|
||||||
pageWidth: number
|
pageWidth: number
|
||||||
) => {
|
) => {
|
||||||
if (mouseState.dragging) {
|
if (mouseState.dragging) {
|
||||||
@ -273,18 +255,21 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
let left = to(pageWidth, x - coordsOffset.x)
|
let left = to(pageWidth, x - coordsOffset.x)
|
||||||
let top = to(pageWidth, y - coordsOffset.y)
|
let top = to(pageWidth, y - coordsOffset.y)
|
||||||
|
|
||||||
const rightLimit = to(pageWidth, rect.width) - drawnField.width
|
setField((prev) => {
|
||||||
const bottomLimit = to(pageWidth, rect.height) - drawnField.height
|
const rightLimit = to(pageWidth, rect.width) - prev!.width
|
||||||
|
const bottomLimit = to(pageWidth, rect.height) - prev!.height
|
||||||
|
|
||||||
if (left < 0) left = 0
|
if (left < 0) left = 0
|
||||||
if (top < 0) top = 0
|
if (top < 0) top = 0
|
||||||
if (left > rightLimit) left = rightLimit
|
if (left > rightLimit) left = rightLimit
|
||||||
if (top > bottomLimit) top = bottomLimit
|
if (top > bottomLimit) top = bottomLimit
|
||||||
|
|
||||||
drawnField.left = left
|
return {
|
||||||
drawnField.top = top
|
...prev!,
|
||||||
|
left,
|
||||||
refreshPdfFiles()
|
top
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -292,73 +277,85 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
/**
|
/**
|
||||||
* Fired when clicked on the resize handle, sets the state for a resize action
|
* Fired when clicked on the resize handle, sets the state for a resize action
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param drawnFieldIndex which we are resizing
|
* @param fieldIndexer which field we are resizing
|
||||||
*/
|
*/
|
||||||
const handleResizePointerDown = (
|
const handleResizePointerDown = useCallback(
|
||||||
event: React.PointerEvent,
|
(event: React.PointerEvent, fieldIndexer: FieldIndexer) => {
|
||||||
fileIndex: number,
|
|
||||||
pageIndex: number,
|
|
||||||
drawnFieldIndex: number
|
|
||||||
) => {
|
|
||||||
// Proceed only if left click
|
// Proceed only if left click
|
||||||
if (event.button !== 0) return
|
if (event.button !== 0) return
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex })
|
event.currentTarget.setPointerCapture(event.pointerId)
|
||||||
|
const [fileIndex, pageIndex, drawnFieldIndex] = fieldIndexer
|
||||||
|
const page = sigitFiles[fileIndex].pages![pageIndex]
|
||||||
|
const drawnField = page.drawnFields[drawnFieldIndex]
|
||||||
|
setIndexer(fieldIndexer)
|
||||||
|
setField({
|
||||||
|
...drawnField,
|
||||||
|
x: drawnField.left,
|
||||||
|
y: drawnField.top
|
||||||
|
})
|
||||||
|
setLastIndexer(fieldIndexer)
|
||||||
setMouseState({
|
setMouseState({
|
||||||
resizing: true
|
resizing: true
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
[sigitFiles]
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resizes the drawn element by the mouse position
|
* Resizes the drawn element by the mouse position
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param drawnField which we are resizing
|
* @param pageWidth pdf value used to scale pointer coordinates
|
||||||
* @param pageWidth pdf value which is used to calculate scaled offset
|
|
||||||
*/
|
*/
|
||||||
const handleResizePointerMove = (
|
const handleResizePointerMove = useCallback(
|
||||||
event: React.PointerEvent,
|
(event: React.PointerEvent, pageWidth: number) => {
|
||||||
drawnField: DrawnField,
|
|
||||||
pageWidth: number
|
|
||||||
) => {
|
|
||||||
if (mouseState.resizing) {
|
if (mouseState.resizing) {
|
||||||
const { x, y } = getPointerCoordinates(
|
|
||||||
event,
|
|
||||||
// currentTarget = span handle
|
// currentTarget = span handle
|
||||||
// 1st parent = drawnField
|
// 1st parent = drawnField
|
||||||
// 2nd parent = img
|
// 2nd parent = img
|
||||||
|
const { x, y } = getPointerCoordinates(
|
||||||
|
event,
|
||||||
event.currentTarget.parentElement?.parentElement
|
event.currentTarget.parentElement?.parentElement
|
||||||
)
|
)
|
||||||
|
|
||||||
const width = to(pageWidth, x) - drawnField.left
|
const pageX = to(pageWidth, x)
|
||||||
const height = to(pageWidth, y) - drawnField.top
|
const pageY = to(pageWidth, y)
|
||||||
|
|
||||||
drawnField.width = width
|
setField((prev) => {
|
||||||
drawnField.height = height
|
const left = pageX < prev!.x ? pageX : prev!.x
|
||||||
|
const top = pageY < prev!.y ? pageY : prev!.y
|
||||||
|
|
||||||
refreshPdfFiles()
|
const width = Math.abs(pageX - prev!.x)
|
||||||
}
|
const height = Math.abs(pageY - prev!.y)
|
||||||
|
return { ...prev!, left, top, width, height }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[mouseState.resizing, to]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePointerUpReleaseCapture = useCallback(
|
||||||
|
(event: React.PointerEvent) => {
|
||||||
|
event.currentTarget.releasePointerCapture(event.pointerId)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the drawn element using the indexes in the params
|
* Removes the drawn element using the indexes in the params
|
||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param pdfFileIndex pdf file index
|
* @param fieldIIndexer [file index, page index, field index]
|
||||||
* @param pdfPageIndex pdf page index
|
|
||||||
* @param drawnFileIndex drawn file index
|
|
||||||
*/
|
*/
|
||||||
const handleRemovePointerDown = (
|
const handleRemovePointerDown = (
|
||||||
event: React.PointerEvent,
|
event: React.PointerEvent,
|
||||||
pdfFileIndex: number,
|
[fileIndex, pageIndex, fieldIndex]: FieldIndexer
|
||||||
pdfPageIndex: number,
|
|
||||||
drawnFileIndex: number
|
|
||||||
) => {
|
) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
const pages = sigitFiles[pdfFileIndex]?.pages
|
updateSigitFiles((draft) => {
|
||||||
if (pages) {
|
draft[fileIndex]?.pages![pageIndex]?.drawnFields?.splice(fieldIndex, 1)
|
||||||
pages[pdfPageIndex]?.drawnFields?.splice(drawnFileIndex, 1)
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -397,28 +394,72 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the pointer coordinates relative to a element in the `event` param
|
* Drawing is finished, resets all the variables used to draw
|
||||||
* @param event PointerEvent
|
|
||||||
* @param customTarget coordinates relative to this element, if not provided
|
|
||||||
* event.target will be used
|
|
||||||
*/
|
*/
|
||||||
const getPointerCoordinates = (
|
const handlePointerUp = useCallback(() => {
|
||||||
event: React.PointerEvent,
|
// Proceed if we have selected something
|
||||||
customTarget?: HTMLElement | null
|
if (indexer) {
|
||||||
) => {
|
// Check if we have the "preview" field state
|
||||||
const target = customTarget ? customTarget : event.currentTarget
|
if (field) {
|
||||||
const rect = target.getBoundingClientRect()
|
// Cancel update if preview field is below the MINIMUM_RECT_SIZE threshhold
|
||||||
|
if (
|
||||||
|
field.width < MINIMUM_RECT_SIZE.width ||
|
||||||
|
field.height < MINIMUM_RECT_SIZE.height
|
||||||
|
) {
|
||||||
|
setIndexer(undefined)
|
||||||
|
setMouseState({})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Clamp X Y within the target
|
const [fileIndex, pageIndex, fieldIndex] = indexer
|
||||||
const x = Math.min(event.clientX, rect.right) - rect.left //x position within the element.
|
|
||||||
const y = Math.min(event.clientY, rect.bottom) - rect.top //y position within the element.
|
|
||||||
|
|
||||||
return {
|
// Add new drawn field to the files
|
||||||
x,
|
if (mouseState.clicked) {
|
||||||
y,
|
updateSigitFiles((draft) => {
|
||||||
rect
|
draft[fileIndex].pages![pageIndex].drawnFields.push(field)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move
|
||||||
|
if (mouseState.dragging) {
|
||||||
|
updateSigitFiles((draft) => {
|
||||||
|
draft[fileIndex].pages![pageIndex].drawnFields[fieldIndex!] = field
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
if (mouseState.resizing) {
|
||||||
|
updateSigitFiles((draft) => {
|
||||||
|
draft[fileIndex].pages![pageIndex].drawnFields[fieldIndex!] = field
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear indexer after applying the update
|
||||||
|
setIndexer(undefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setMouseState({})
|
||||||
|
}, [
|
||||||
|
field,
|
||||||
|
indexer,
|
||||||
|
mouseState.clicked,
|
||||||
|
mouseState.dragging,
|
||||||
|
mouseState.resizing,
|
||||||
|
updateSigitFiles
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawing events
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('pointerup', handlePointerUp)
|
||||||
|
window.addEventListener('pointercancel', handlePointerUp)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('pointerup', handlePointerUp)
|
||||||
|
window.removeEventListener('pointercancel', handlePointerUp)
|
||||||
|
}
|
||||||
|
}, [handlePointerUp])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the pdf pages and drawing elements
|
* Renders the pdf pages and drawing elements
|
||||||
@ -430,6 +471,13 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{file.pages?.map((page, pageIndex: number) => {
|
{file.pages?.map((page, pageIndex: number) => {
|
||||||
|
let isPageIndexerActive = false
|
||||||
|
if (indexer) {
|
||||||
|
const [fi, pi, di] = indexer
|
||||||
|
isPageIndexerActive =
|
||||||
|
fi === fileIndex && pi === pageIndex && typeof di === 'undefined'
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={pageIndex}
|
key={pageIndex}
|
||||||
@ -438,32 +486,68 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
onKeyDown={(event) => handleEscapeButtonDown(event)}
|
onKeyDown={(event) => handleEscapeButtonDown(event)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
onPointerMove={(event) => {
|
|
||||||
handlePointerMove(event, page)
|
|
||||||
}}
|
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
handlePointerDown(event, page, fileIndex, pageIndex)
|
handlePointerDown(event, [fileIndex, pageIndex], page.width)
|
||||||
}}
|
}}
|
||||||
|
onPointerMove={(event) => {
|
||||||
|
handlePointerMove(event, page.width)
|
||||||
|
}}
|
||||||
|
onPointerUp={handlePointerUpReleaseCapture}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
src={page.image}
|
src={page.image}
|
||||||
alt={`page ${pageIndex + 1} of ${file.name}`}
|
alt={`page ${pageIndex + 1} of ${file.name}`}
|
||||||
/>
|
/>
|
||||||
|
{isPageIndexerActive && field && (
|
||||||
|
<div
|
||||||
|
className={styles.drawingRectanglePreview}
|
||||||
|
style={{
|
||||||
|
backgroundColor: field.counterpart
|
||||||
|
? `#${npubToHex(field.counterpart)?.substring(0, 6)}4b`
|
||||||
|
: undefined,
|
||||||
|
outlineColor: field.counterpart
|
||||||
|
? `#${npubToHex(field.counterpart)?.substring(0, 6)}`
|
||||||
|
: undefined,
|
||||||
|
left: inPx(from(page.width, field.left)),
|
||||||
|
top: inPx(from(page.width, field.top)),
|
||||||
|
width: inPx(from(page.width, field.width)),
|
||||||
|
height: inPx(from(page.width, field.height))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`file-mark ${styles.placeholder}`}
|
||||||
|
style={{
|
||||||
|
fontFamily: FONT_TYPE,
|
||||||
|
fontSize: inPx(from(page.width, FONT_SIZE))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getToolboxLabelByMarkType(field.type) || 'placeholder'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{page.drawnFields.map((drawnField, drawnFieldIndex: number) => {
|
{page.drawnFields.map((drawnField, drawnFieldIndex: number) => {
|
||||||
|
let isFieldIndexerActive = false
|
||||||
|
if (indexer) {
|
||||||
|
const [fi, pi, di] = indexer
|
||||||
|
isFieldIndexerActive =
|
||||||
|
fi === fileIndex &&
|
||||||
|
pi === pageIndex &&
|
||||||
|
di === drawnFieldIndex
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={drawnFieldIndex}
|
key={drawnFieldIndex}
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
handleDrawnFieldPointerDown(
|
handleDrawnFieldPointerDown(event, [
|
||||||
event,
|
|
||||||
fileIndex,
|
fileIndex,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
drawnFieldIndex
|
drawnFieldIndex
|
||||||
)
|
])
|
||||||
}
|
}
|
||||||
onPointerMove={(event) => {
|
onPointerMove={(event) => {
|
||||||
handleDrawnFieldPointerMove(event, drawnField, page.width)
|
handleDrawnFieldPointerMove(event, page.width)
|
||||||
}}
|
}}
|
||||||
|
onPointerUp={handlePointerUpReleaseCapture}
|
||||||
className={styles.drawingRectangle}
|
className={styles.drawingRectangle}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: drawnField.counterpart
|
backgroundColor: drawnField.counterpart
|
||||||
@ -472,12 +556,29 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
outlineColor: drawnField.counterpart
|
outlineColor: drawnField.counterpart
|
||||||
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}`
|
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}`
|
||||||
: undefined,
|
: undefined,
|
||||||
|
...(isFieldIndexerActive && field
|
||||||
|
? {
|
||||||
|
left: inPx(from(page.width, field.left)),
|
||||||
|
top: inPx(from(page.width, field.top)),
|
||||||
|
width: inPx(from(page.width, field.width)),
|
||||||
|
height: inPx(from(page.width, field.height))
|
||||||
|
}
|
||||||
|
: {
|
||||||
left: inPx(from(page.width, drawnField.left)),
|
left: inPx(from(page.width, drawnField.left)),
|
||||||
top: inPx(from(page.width, drawnField.top)),
|
top: inPx(from(page.width, drawnField.top)),
|
||||||
width: inPx(from(page.width, drawnField.width)),
|
width: inPx(from(page.width, drawnField.width)),
|
||||||
height: inPx(from(page.width, drawnField.height)),
|
height: inPx(from(page.width, drawnField.height))
|
||||||
|
}),
|
||||||
|
|
||||||
pointerEvents: mouseState.clicked ? 'none' : 'all',
|
pointerEvents: mouseState.clicked ? 'none' : 'all',
|
||||||
touchAction: 'none',
|
touchAction: 'none',
|
||||||
|
zIndex: isActiveDrawnField(
|
||||||
|
fileIndex,
|
||||||
|
pageIndex,
|
||||||
|
drawnFieldIndex
|
||||||
|
)
|
||||||
|
? 60
|
||||||
|
: undefined,
|
||||||
opacity:
|
opacity:
|
||||||
mouseState.dragging &&
|
mouseState.dragging &&
|
||||||
isActiveDrawnField(
|
isActiveDrawnField(
|
||||||
@ -501,37 +602,36 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
handleResizePointerDown(
|
handleResizePointerDown(event, [
|
||||||
event,
|
|
||||||
fileIndex,
|
fileIndex,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
drawnFieldIndex
|
drawnFieldIndex
|
||||||
)
|
])
|
||||||
}
|
}
|
||||||
onPointerMove={(event) => {
|
onPointerMove={(event) => {
|
||||||
handleResizePointerMove(event, drawnField, page.width)
|
handleResizePointerMove(event, page.width)
|
||||||
}}
|
}}
|
||||||
|
onPointerUp={handlePointerUpReleaseCapture}
|
||||||
className={styles.resizeHandle}
|
className={styles.resizeHandle}
|
||||||
style={{
|
style={{
|
||||||
background:
|
...(mouseState.resizing &&
|
||||||
mouseState.resizing &&
|
|
||||||
isActiveDrawnField(
|
isActiveDrawnField(
|
||||||
fileIndex,
|
fileIndex,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
drawnFieldIndex
|
drawnFieldIndex
|
||||||
)
|
) && {
|
||||||
? 'var(--primary-main)'
|
cursor: 'grabbing',
|
||||||
: undefined
|
opacity: 0.1
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
></span>
|
></span>
|
||||||
<span
|
<span
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
handleRemovePointerDown(
|
handleRemovePointerDown(event, [
|
||||||
event,
|
|
||||||
fileIndex,
|
fileIndex,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
drawnFieldIndex
|
drawnFieldIndex
|
||||||
)
|
])
|
||||||
}}
|
}}
|
||||||
className={styles.removeHandle}
|
className={styles.removeHandle}
|
||||||
>
|
>
|
||||||
@ -569,23 +669,26 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
drawnField.counterpart = event.target.value
|
drawnField.counterpart = event.target.value
|
||||||
setLastSigner(event.target.value)
|
setLastSigner(event.target.value)
|
||||||
refreshPdfFiles()
|
|
||||||
}}
|
}}
|
||||||
labelId="counterparts"
|
labelId="counterparts"
|
||||||
label="Counterparts"
|
label="Counterparts"
|
||||||
sx={{
|
sx={{
|
||||||
background: 'white'
|
background: 'white'
|
||||||
}}
|
}}
|
||||||
renderValue={(value) =>
|
renderValue={(value) => (
|
||||||
renderCounterpartValue(value)
|
<Counterpart
|
||||||
}
|
npub={value}
|
||||||
|
metadata={metadata}
|
||||||
|
signers={signers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{signers.map((signer, index) => {
|
{signers.map((signer, index) => {
|
||||||
const npub = hexToNpub(signer.pubkey)
|
const npub = hexToNpub(signer.pubkey)
|
||||||
const metadata = props.metadata[signer.pubkey]
|
const profileMetadata = metadata[signer.pubkey]
|
||||||
const displayValue = getProfileUsername(
|
const displayValue = getProfileUsername(
|
||||||
npub,
|
npub,
|
||||||
metadata
|
profileMetadata
|
||||||
)
|
)
|
||||||
// make current signers dropdown visible
|
// make current signers dropdown visible
|
||||||
if (
|
if (
|
||||||
@ -604,7 +707,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
<MenuItem key={index} value={npub}>
|
<MenuItem key={index} value={npub}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<AvatarIconButton
|
<AvatarIconButton
|
||||||
src={metadata?.picture}
|
src={profileMetadata?.picture}
|
||||||
hexKey={signer.pubkey}
|
hexKey={signer.pubkey}
|
||||||
aria-label={`account of user ${displayValue}`}
|
aria-label={`account of user ${displayValue}`}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@ -635,58 +738,24 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderCounterpartValue = (npub: string) => {
|
|
||||||
let displayValue = _.truncate(npub, { length: 16 })
|
|
||||||
|
|
||||||
const signer = signers.find((u) => u.pubkey === npubToHex(npub))
|
|
||||||
if (signer) {
|
|
||||||
const metadata = props.metadata[signer.pubkey]
|
|
||||||
displayValue = getProfileUsername(npub, metadata)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.counterpartSelectValue}>
|
|
||||||
<AvatarIconButton
|
|
||||||
src={props.metadata[signer.pubkey]?.picture}
|
|
||||||
hexKey={signer.pubkey || undefined}
|
|
||||||
sx={{
|
|
||||||
padding: 0,
|
|
||||||
marginRight: '6px',
|
|
||||||
'> img': {
|
|
||||||
width: '21px',
|
|
||||||
height: '21px'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{displayValue}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return displayValue
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="files-wrapper">
|
<div className="files-wrapper">
|
||||||
{sigitFiles.map((file, i) => {
|
{sigitFiles.length > 0 &&
|
||||||
return (
|
sigitFiles
|
||||||
|
.map<React.ReactNode>((file, i) =>
|
||||||
|
file.isPdf ? (
|
||||||
<React.Fragment key={file.name}>
|
<React.Fragment key={file.name}>
|
||||||
<div className="file-wrapper" id={`file-${file.name}`}>
|
{getPdfPages(file, i)}
|
||||||
{file.isPdf && getPdfPages(file, i)}
|
|
||||||
{file.isImage && (
|
|
||||||
<img
|
|
||||||
className="file-image"
|
|
||||||
src={file.objectUrl}
|
|
||||||
alt={file.name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!(file.isPdf || file.isImage) && (
|
|
||||||
<ExtensionFileBox extension={file.extension} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{i < sigitFiles.length - 1 && <FileDivider />}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<FileItem key={file.name} file={file} />
|
||||||
)
|
)
|
||||||
})}
|
)
|
||||||
|
.reduce((prev, curr, i) => [
|
||||||
|
prev,
|
||||||
|
<FileDivider key={`separator-${i}`} />,
|
||||||
|
curr
|
||||||
|
])}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.counterpartSelectValue {
|
||||||
|
display: flex;
|
||||||
|
}
|
46
src/components/DrawPDFFields/internal/Counterpart.tsx
Normal file
46
src/components/DrawPDFFields/internal/Counterpart.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { ProfileMetadata, User } from '../../../types'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { npubToHex, getProfileUsername } from '../../../utils'
|
||||||
|
import { AvatarIconButton } from '../../UserAvatarIconButton'
|
||||||
|
import styles from './Counterpart.module.scss'
|
||||||
|
|
||||||
|
interface CounterpartProps {
|
||||||
|
npub: string
|
||||||
|
metadata: {
|
||||||
|
[key: string]: ProfileMetadata
|
||||||
|
}
|
||||||
|
signers: User[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Counterpart = React.memo(
|
||||||
|
({ npub, metadata, signers }: CounterpartProps) => {
|
||||||
|
let displayValue = _.truncate(npub, { length: 16 })
|
||||||
|
|
||||||
|
const signer = signers.find((u) => u.pubkey === npubToHex(npub))
|
||||||
|
if (signer) {
|
||||||
|
const signerMetadata = metadata[signer.pubkey]
|
||||||
|
displayValue = getProfileUsername(npub, signerMetadata)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.counterpartSelectValue}>
|
||||||
|
<AvatarIconButton
|
||||||
|
src={signerMetadata.picture}
|
||||||
|
hexKey={signer.pubkey || undefined}
|
||||||
|
sx={{
|
||||||
|
padding: 0,
|
||||||
|
marginRight: '6px',
|
||||||
|
'> img': {
|
||||||
|
width: '21px',
|
||||||
|
height: '21px'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{displayValue}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayValue
|
||||||
|
}
|
||||||
|
)
|
19
src/components/DrawPDFFields/internal/FileItem.tsx
Normal file
19
src/components/DrawPDFFields/internal/FileItem.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { SigitFile } from '../../../utils/file'
|
||||||
|
import { ExtensionFileBox } from '../../ExtensionFileBox'
|
||||||
|
import { ImageItem } from './ImageItem'
|
||||||
|
|
||||||
|
interface FileItemProps {
|
||||||
|
file: SigitFile
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileItem = React.memo(({ file }: FileItemProps) => {
|
||||||
|
const content = <ExtensionFileBox extension={file.extension} />
|
||||||
|
if (file.isImage) return <ImageItem file={file} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={file.name} className="file-wrapper" id={`file-${file.name}`}>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
10
src/components/DrawPDFFields/internal/ImageItem.tsx
Normal file
10
src/components/DrawPDFFields/internal/ImageItem.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { SigitFile } from '../../../utils/file'
|
||||||
|
|
||||||
|
interface ImageItemProps {
|
||||||
|
file: SigitFile
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImageItem = React.memo(({ file }: ImageItemProps) => {
|
||||||
|
return <img className="file-image" src={file.objectUrl} alt={file.name} />
|
||||||
|
})
|
@ -47,7 +47,7 @@
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 1px solid rgb(160, 160, 160);
|
border: 1px solid rgb(160, 160, 160);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: nwse-resize;
|
cursor: grab;
|
||||||
|
|
||||||
// Increase the area a bit so it's easier to click
|
// Increase the area a bit so it's easier to click
|
||||||
&::after {
|
&::after {
|
||||||
@ -85,10 +85,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.counterpartSelectValue {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.counterpartAvatar {
|
.counterpartAvatar {
|
||||||
img {
|
img {
|
||||||
width: 21px;
|
width: 21px;
|
||||||
@ -107,3 +103,16 @@
|
|||||||
outline: 1px dotted #01aaad;
|
outline: 1px dotted #01aaad;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drawingRectanglePreview {
|
||||||
|
position: absolute;
|
||||||
|
outline: 1px solid;
|
||||||
|
z-index: 50;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: none;
|
||||||
|
touch-action: none;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
z-index: 50;
|
z-index: 70;
|
||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(10px);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
@ -45,18 +45,17 @@ const PdfView = ({
|
|||||||
const filterMarksByFile = (marks: Mark[], hash: string): Mark[] => {
|
const filterMarksByFile = (marks: Mark[], hash: string): Mark[] => {
|
||||||
return marks.filter((mark) => mark.pdfFileHash === hash)
|
return marks.filter((mark) => mark.pdfFileHash === hash)
|
||||||
}
|
}
|
||||||
const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean =>
|
|
||||||
index !== files.length - 1
|
|
||||||
return (
|
return (
|
||||||
<div className="files-wrapper">
|
<div className="files-wrapper">
|
||||||
{files.length > 0 ? (
|
{files.length > 0 ? (
|
||||||
files.map((currentUserFile, index, arr) => {
|
files
|
||||||
|
.map<React.ReactNode>((currentUserFile) => {
|
||||||
const { hash, file, id } = currentUserFile
|
const { hash, file, id } = currentUserFile
|
||||||
|
|
||||||
if (!hash) return
|
if (!hash) return
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
|
||||||
<div
|
<div
|
||||||
|
key={`file-${file.name}`}
|
||||||
id={file.name}
|
id={file.name}
|
||||||
className="file-wrapper"
|
className="file-wrapper"
|
||||||
ref={(el) => (pdfRefs.current[id] = el)}
|
ref={(el) => (pdfRefs.current[id] = el)}
|
||||||
@ -70,10 +69,13 @@ const PdfView = ({
|
|||||||
otherUserMarks={filterMarksByFile(otherUserMarks, hash)}
|
otherUserMarks={filterMarksByFile(otherUserMarks, hash)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isNotLastPdfFile(index, arr) && <FileDivider />}
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.reduce((prev, curr, i) => [
|
||||||
|
prev,
|
||||||
|
<FileDivider key={`separator-${i}`} />,
|
||||||
|
curr
|
||||||
|
])
|
||||||
) : (
|
) : (
|
||||||
<LoadingSpinner variant="small" />
|
<LoadingSpinner variant="small" />
|
||||||
)}
|
)}
|
||||||
|
@ -79,10 +79,11 @@ import {
|
|||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { getSigitFile, SigitFile } from '../../utils/file.ts'
|
import { getSigitFile, SigitFile } from '../../utils/file.ts'
|
||||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
import { Autocomplete } from '@mui/lab'
|
import { Autocomplete } from '@mui/material'
|
||||||
import _, { truncate } from 'lodash'
|
import _, { truncate } from 'lodash'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { AvatarIconButton } from '../../components/UserAvatarIconButton'
|
import { AvatarIconButton } from '../../components/UserAvatarIconButton'
|
||||||
|
import { useImmer } from 'use-immer'
|
||||||
|
|
||||||
type FoundUser = Event & { npub: string }
|
type FoundUser = Event & { npub: string }
|
||||||
|
|
||||||
@ -98,7 +99,7 @@ export const CreatePage = () => {
|
|||||||
|
|
||||||
const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
|
const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
|
||||||
|
|
||||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([...uploadedFiles])
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const handleUploadButtonClick = () => {
|
const handleUploadButtonClick = () => {
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
@ -123,7 +124,7 @@ export const CreatePage = () => {
|
|||||||
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
|
const [drawnFiles, updateDrawnFiles] = useImmer<SigitFile[]>([])
|
||||||
const [parsingPdf, setIsParsing] = useState<boolean>(false)
|
const [parsingPdf, setIsParsing] = useState<boolean>(false)
|
||||||
|
|
||||||
const searchFieldRef = useRef<HTMLInputElement>(null)
|
const searchFieldRef = useRef<HTMLInputElement>(null)
|
||||||
@ -295,8 +296,28 @@ export const CreatePage = () => {
|
|||||||
selectedFiles,
|
selectedFiles,
|
||||||
getSigitFile
|
getSigitFile
|
||||||
)
|
)
|
||||||
|
updateDrawnFiles((draft) => {
|
||||||
|
// Existing files are untouched
|
||||||
|
|
||||||
setDrawnFiles(files)
|
// Handle removed files
|
||||||
|
// Remove in reverse to avoid index issues
|
||||||
|
for (let i = draft.length - 1; i >= 0; i--) {
|
||||||
|
if (
|
||||||
|
!files.some(
|
||||||
|
(f) => f.name === draft[i].name && f.size === draft[i].size
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
draft.splice(i, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new files
|
||||||
|
files.forEach((f) => {
|
||||||
|
if (!draft.some((d) => d.name === f.name && d.size === f.size)) {
|
||||||
|
draft.push(f)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsParsing(true)
|
setIsParsing(true)
|
||||||
@ -305,7 +326,7 @@ export const CreatePage = () => {
|
|||||||
setIsParsing(false)
|
setIsParsing(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [selectedFiles])
|
}, [selectedFiles, updateDrawnFiles])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the drawing tool
|
* Changes the drawing tool
|
||||||
@ -357,12 +378,6 @@ export const CreatePage = () => {
|
|||||||
})
|
})
|
||||||
}, [metadata, users])
|
}, [metadata, users])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (uploadedFiles) {
|
|
||||||
setSelectedFiles([...uploadedFiles])
|
|
||||||
}
|
|
||||||
}, [uploadedFiles])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (usersPubkey) {
|
if (usersPubkey) {
|
||||||
setUsers((prev) => {
|
setUsers((prev) => {
|
||||||
@ -516,7 +531,7 @@ export const CreatePage = () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
setDrawnFiles(drawnFilesCopy)
|
updateDrawnFiles(drawnFilesCopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -540,11 +555,16 @@ export const CreatePage = () => {
|
|||||||
const files = Array.from(event.target.files)
|
const files = Array.from(event.target.files)
|
||||||
|
|
||||||
// Remove duplicates based on the file.name
|
// Remove duplicates based on the file.name
|
||||||
setSelectedFiles((p) =>
|
setSelectedFiles((p) => {
|
||||||
[...p, ...files].filter(
|
const unique = [...p, ...files].filter(
|
||||||
(file, i, array) => i === array.findIndex((t) => t.name === file.name)
|
(file, i, array) => i === array.findIndex((t) => t.name === file.name)
|
||||||
)
|
)
|
||||||
)
|
navigate('.', {
|
||||||
|
state: { uploadedFiles: unique },
|
||||||
|
replace: true
|
||||||
|
})
|
||||||
|
return unique
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -558,9 +578,14 @@ export const CreatePage = () => {
|
|||||||
) => {
|
) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
setSelectedFiles((prevFiles) =>
|
setSelectedFiles((prevFiles) => {
|
||||||
prevFiles.filter((file) => file.name !== fileToRemove.name)
|
const files = prevFiles.filter((file) => file.name !== fileToRemove.name)
|
||||||
)
|
navigate('.', {
|
||||||
|
state: { uploadedFiles: files },
|
||||||
|
replace: true
|
||||||
|
})
|
||||||
|
return files
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate inputs before proceeding
|
// Validate inputs before proceeding
|
||||||
@ -1025,13 +1050,11 @@ export const CreatePage = () => {
|
|||||||
return JSON.parse(event.content)
|
return JSON.parse(event.content)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return undefined
|
return undefined
|
||||||
console.error(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
|
||||||
<Container className={styles.container}>
|
<Container className={styles.container}>
|
||||||
<StickySideColumns
|
<StickySideColumns
|
||||||
left={
|
left={
|
||||||
@ -1244,19 +1267,17 @@ export const CreatePage = () => {
|
|||||||
centerIcon={faFile}
|
centerIcon={faFile}
|
||||||
rightIcon={faToolbox}
|
rightIcon={faToolbox}
|
||||||
>
|
>
|
||||||
{parsingPdf ? (
|
|
||||||
<LoadingSpinner variant="small" />
|
|
||||||
) : (
|
|
||||||
<DrawPDFFields
|
<DrawPDFFields
|
||||||
users={users}
|
users={users}
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
selectedTool={selectedTool}
|
selectedTool={selectedTool}
|
||||||
sigitFiles={drawnFiles}
|
sigitFiles={drawnFiles}
|
||||||
setSigitFiles={setDrawnFiles}
|
updateSigitFiles={updateDrawnFiles}
|
||||||
/>
|
/>
|
||||||
)}
|
{parsingPdf && <LoadingSpinner variant="small" />}
|
||||||
</StickySideColumns>
|
</StickySideColumns>
|
||||||
</Container>
|
</Container>
|
||||||
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user