diff --git a/package-lock.json b/package-lock.json index 2b59ffe..af41ed4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,8 @@ "react-toastify": "10.0.4", "redux": "5.0.1", "signature_pad": "^5.0.4", - "tseep": "1.2.1" + "tseep": "1.2.1", + "use-immer": "^0.11.0" }, "devDependencies": { "@types/crypto-js": "^4.2.2", @@ -8591,6 +8592,16 @@ "dev": true, "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": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index 1c4b0aa..60c9a2b 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "react-toastify": "10.0.4", "redux": "5.0.1", "signature_pad": "^5.0.4", - "tseep": "1.2.1" + "tseep": "1.2.1", + "use-immer": "^0.11.0" }, "devDependencies": { "@types/crypto-js": "^4.2.2", diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index b9fd162..2a43b3b 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -8,23 +8,24 @@ import { Select } from '@mui/material' 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 { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing' +import { MouseState, DrawnField, DrawTool } from '../../types/drawing' import { hexToNpub, npubToHex, getProfileUsername } from '../../utils' import { SigitFile } from '../../utils/file' import { getToolboxLabelByMarkType } from '../../utils/mark' -import { FileDivider } from '../FileDivider' -import { ExtensionFileBox } from '../ExtensionFileBox' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf' import { useScale } from '../../hooks/useScale' import { AvatarIconButton } from '../UserAvatarIconButton' import { UserAvatar } from '../UserAvatar' -import _ from 'lodash' +import { Updater } from 'use-immer' +import { FileItem } from './internal/FileItem' +import { FileDivider } from '../FileDivider' +import { Counterpart } from './internal/Counterpart' const MINIMUM_RECT_SIZE = { - width: 21, - height: 21 + width: 10, + height: 10 } as const const DEFAULT_START_SIZE = { @@ -36,16 +37,25 @@ interface HideSignersForDrawnField { [key: number]: boolean } -interface Props { +type PageIndexer = [file: number, page: number] +type FieldIndexer = [...PageIndexer, field: number] + +interface DrawPdfFieldsProps { users: User[] metadata: { [key: string]: ProfileMetadata } sigitFiles: SigitFile[] - setSigitFiles: React.Dispatch> + updateSigitFiles: Updater selectedTool?: DrawTool } -export const DrawPDFFields = (props: Props) => { - const { selectedTool, sigitFiles, setSigitFiles, users } = props +export const DrawPDFFields = ({ + selectedTool, + metadata, + sigitFiles, + updateSigitFiles, + users +}: DrawPdfFieldsProps) => { + const { to, from } = useScale() const signers = users.filter((u) => u.role === UserRole.signer) const defaultSignerNpub = signers.length ? hexToNpub(signers[0].pubkey) : '' @@ -58,158 +68,124 @@ export const DrawPDFFields = (props: Props) => { * @param pubkeys * @returns available pubkey or empty string */ - const getAvailableSigner = (...pubkeys: string[]) => { - const availableSigner: string | undefined = pubkeys.find((pubkey) => - signers.some((s) => s.pubkey === npubToHex(pubkey)) - ) - return availableSigner || '' - } + const getAvailableSigner = useCallback( + (...pubkeys: string[]) => { + const availableSigner: string | undefined = pubkeys.find((pubkey) => + signers.some((s) => s.pubkey === npubToHex(pubkey)) + ) + return availableSigner || '' + }, + [signers] + ) - const { to, from } = useScale() - - const [mouseState, setMouseState] = useState({ - clicked: false - }) - - const [activeDrawnField, setActiveDrawnField] = useState<{ - fileIndex: number - pageIndex: number - drawnFieldIndex: number - }>() + const [mouseState, setMouseState] = useState({}) + const [indexer, setIndexer] = useState() + const [field, setField] = useState< + DrawnField & { + x: number + y: number + } + >() + const [lastIndexer, setLastIndexer] = useState() const isActiveDrawnField = ( fileIndex: number, pageIndex: number, drawnFieldIndex: number ) => - activeDrawnField?.fileIndex === fileIndex && - activeDrawnField?.pageIndex === pageIndex && - activeDrawnField?.drawnFieldIndex === drawnFieldIndex + lastIndexer && + lastIndexer[0] === fileIndex && + 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(() => { - window.addEventListener('pointerup', handlePointerUp) - window.addEventListener('pointercancel', handlePointerUp) + const getPointerCoordinates = ( + event: React.PointerEvent, + customTarget?: HTMLElement | null + ) => { + const target = customTarget ? customTarget : event.currentTarget + const rect = target.getBoundingClientRect() - return () => { - window.removeEventListener('pointerup', handlePointerUp) - window.removeEventListener('pointercancel', handlePointerUp) + // Clamp X Y within the target + const x = Math.max(0, Math.min(event.clientX, rect.right) - rect.left) //x position within the element. + 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 - * Creates new drawnElement and pushes in the array - * It is re rendered and visible right away + * Creates new drawnElement * * @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 = ( - event: React.PointerEvent, - page: PdfPage, - fileIndex: number, - pageIndex: number - ) => { - // Proceed only if left click - if (event.button !== 0) return - - if (!selectedTool) { - return - } - - const { x, y } = getPointerCoordinates(event) - - const newField: DrawnField = { - left: to(page.width, x), - top: to(page.width, y), - width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width, - height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height, - counterpart: getAvailableSigner(lastSigner, defaultSignerNpub), - type: selectedTool.identifier - } - - page.drawnFields.push(newField) - - setActiveDrawnField({ - fileIndex, - pageIndex, - drawnFieldIndex: page.drawnFields.length - 1 - }) - setMouseState((prev) => { - return { - ...prev, - clicked: true - } - }) - } - - /** - * Drawing is finished, resets all the variables used to draw - * @param event Pointer event - */ - const handlePointerUp = () => { - sigitFiles.forEach((s) => { - 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') { - 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 handlePointerDown = useCallback( + ( + event: React.PointerEvent, + pageIndexer: PageIndexer, + pageWidth: number + ) => { + // Proceed only if left click + if (event.button !== 0) return + if (!selectedTool) return + event.currentTarget.setPointerCapture(event.pointerId) + const counterpart = getAvailableSigner(lastSigner, defaultSignerNpub) const { x, y } = getPointerCoordinates(event) - const width = to(page.width, x) - lastDrawnField.left - const height = to(page.width, y) - lastDrawnField.top + setIndexer(pageIndexer) + setField({ + x: to(pageWidth, x), + y: to(pageWidth, y), + left: to(pageWidth, x), + top: to(pageWidth, y), + width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width, + height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height, + type: selectedTool.identifier, + counterpart + }) + setMouseState({ + clicked: true + }) + }, + [defaultSignerNpub, getAvailableSigner, lastSigner, selectedTool, to] + ) - lastDrawnField.width = width - lastDrawnField.height = height + /** + * After {@link handlePointerDown} creates an drawing element, this function + * alters the newly created drawing element, resizing it while pointer moves + * @param event Pointer event + * @param pageWidth pdf value used to scale pointer coordinates + */ + const handlePointerMove = useCallback( + (event: React.PointerEvent, pageWidth: number) => { + if (mouseState.clicked && selectedTool && event.pointerType === 'mouse') { + const { x, y } = getPointerCoordinates(event) + const pageX = to(pageWidth, x) + const pageY = to(pageWidth, y) - const currentDrawnFields = page.drawnFields + // Calculate left, top, width, and height based on direction + setField((prev) => { + const left = pageX < prev!.x ? pageX : prev!.x + const top = pageY < prev!.y ? pageY : prev!.y - currentDrawnFields[lastElementIndex] = lastDrawnField - - refreshPdfFiles() - } - } + const width = Math.abs(pageX - prev!.x) + const height = Math.abs(pageY - prev!.y) + return { ...prev!, left, top, width, height } + }) + } + }, + [mouseState.clicked, selectedTool, to] + ) /** * Fired when event happens on the drawn element which will be moved @@ -219,22 +195,30 @@ export const DrawPDFFields = (props: Props) => { * y - offsetY * * @param event Pointer event - * @param drawnFieldIndex Which we are moving + * @param fieldIndexer Which field we are moving */ const handleDrawnFieldPointerDown = ( event: React.PointerEvent, - fileIndex: number, - pageIndex: number, - drawnFieldIndex: number + fieldIndexer: FieldIndexer ) => { event.stopPropagation() // Proceed only if left click if (event.button !== 0) return + event.currentTarget.setPointerCapture(event.pointerId) 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({ dragging: true, clicked: false, @@ -244,7 +228,7 @@ export const DrawPDFFields = (props: Props) => { } }) - // make signers dropdown visible + // Make signers dropdown visible setHideSignersForDrawnField((prev) => ({ ...prev, [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) * @param event Pointer event - * @param drawnField which we are moving - * @param pageWidth pdf value which is used to calculate scaled offset + * @param pageWidth pdf value used to scale pointer coordinates */ const handleDrawnFieldPointerMove = ( event: React.PointerEvent, - drawnField: DrawnField, pageWidth: number ) => { if (mouseState.dragging) { @@ -273,18 +255,21 @@ export const DrawPDFFields = (props: Props) => { let left = to(pageWidth, x - coordsOffset.x) let top = to(pageWidth, y - coordsOffset.y) - const rightLimit = to(pageWidth, rect.width) - drawnField.width - const bottomLimit = to(pageWidth, rect.height) - drawnField.height + setField((prev) => { + const rightLimit = to(pageWidth, rect.width) - prev!.width + const bottomLimit = to(pageWidth, rect.height) - prev!.height - if (left < 0) left = 0 - if (top < 0) top = 0 - if (left > rightLimit) left = rightLimit - if (top > bottomLimit) top = bottomLimit + if (left < 0) left = 0 + if (top < 0) top = 0 + if (left > rightLimit) left = rightLimit + if (top > bottomLimit) top = bottomLimit - drawnField.left = left - drawnField.top = top - - refreshPdfFiles() + return { + ...prev!, + left, + top + } + }) } } } @@ -292,73 +277,85 @@ export const DrawPDFFields = (props: Props) => { /** * Fired when clicked on the resize handle, sets the state for a resize action * @param event Pointer event - * @param drawnFieldIndex which we are resizing + * @param fieldIndexer which field we are resizing */ - const handleResizePointerDown = ( - event: React.PointerEvent, - fileIndex: number, - pageIndex: number, - drawnFieldIndex: number - ) => { - // Proceed only if left click - if (event.button !== 0) return - event.stopPropagation() + const handleResizePointerDown = useCallback( + (event: React.PointerEvent, fieldIndexer: FieldIndexer) => { + // Proceed only if left click + if (event.button !== 0) return + event.stopPropagation() - setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex }) - setMouseState({ - resizing: true - }) - } + 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({ + resizing: true + }) + }, + [sigitFiles] + ) /** * Resizes the drawn element by the mouse position * @param event Pointer event - * @param drawnField which we are resizing - * @param pageWidth pdf value which is used to calculate scaled offset + * @param pageWidth pdf value used to scale pointer coordinates */ - const handleResizePointerMove = ( - event: React.PointerEvent, - drawnField: DrawnField, - pageWidth: number - ) => { - if (mouseState.resizing) { - const { x, y } = getPointerCoordinates( - event, + const handleResizePointerMove = useCallback( + (event: React.PointerEvent, pageWidth: number) => { + if (mouseState.resizing) { // currentTarget = span handle // 1st parent = drawnField // 2nd parent = img - event.currentTarget.parentElement?.parentElement - ) + const { x, y } = getPointerCoordinates( + event, + event.currentTarget.parentElement?.parentElement + ) - const width = to(pageWidth, x) - drawnField.left - const height = to(pageWidth, y) - drawnField.top + const pageX = to(pageWidth, x) + const pageY = to(pageWidth, y) - drawnField.width = width - drawnField.height = height + setField((prev) => { + 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 * @param event Pointer event - * @param pdfFileIndex pdf file index - * @param pdfPageIndex pdf page index - * @param drawnFileIndex drawn file index + * @param fieldIIndexer [file index, page index, field index] */ const handleRemovePointerDown = ( event: React.PointerEvent, - pdfFileIndex: number, - pdfPageIndex: number, - drawnFileIndex: number + [fileIndex, pageIndex, fieldIndex]: FieldIndexer ) => { event.stopPropagation() - const pages = sigitFiles[pdfFileIndex]?.pages - if (pages) { - pages[pdfPageIndex]?.drawnFields?.splice(drawnFileIndex, 1) - } + updateSigitFiles((draft) => { + draft[fileIndex]?.pages![pageIndex]?.drawnFields?.splice(fieldIndex, 1) + }) } /** @@ -397,28 +394,72 @@ export const DrawPDFFields = (props: Props) => { } /** - * 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 + * Drawing is finished, resets all the variables used to draw */ - const getPointerCoordinates = ( - event: React.PointerEvent, - customTarget?: HTMLElement | null - ) => { - const target = customTarget ? customTarget : event.currentTarget - const rect = target.getBoundingClientRect() + const handlePointerUp = useCallback(() => { + // Proceed if we have selected something + if (indexer) { + // Check if we have the "preview" field state + if (field) { + // 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 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. + const [fileIndex, pageIndex, fieldIndex] = indexer - return { - x, - y, - rect + // Add new drawn field to the files + if (mouseState.clicked) { + updateSigitFiles((draft) => { + 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 @@ -430,6 +471,13 @@ export const DrawPDFFields = (props: Props) => { return ( <> {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 (
{ onKeyDown={(event) => handleEscapeButtonDown(event)} > { - handlePointerMove(event, page) - }} onPointerDown={(event) => { - handlePointerDown(event, page, fileIndex, pageIndex) + handlePointerDown(event, [fileIndex, pageIndex], page.width) }} + onPointerMove={(event) => { + handlePointerMove(event, page.width) + }} + onPointerUp={handlePointerUpReleaseCapture} draggable="false" src={page.image} alt={`page ${pageIndex + 1} of ${file.name}`} /> - + {isPageIndexerActive && field && ( +
+
+ {getToolboxLabelByMarkType(field.type) || 'placeholder'} +
+
+ )} {page.drawnFields.map((drawnField, drawnFieldIndex: number) => { + let isFieldIndexerActive = false + if (indexer) { + const [fi, pi, di] = indexer + isFieldIndexerActive = + fi === fileIndex && + pi === pageIndex && + di === drawnFieldIndex + } + return (
- handleDrawnFieldPointerDown( - event, + handleDrawnFieldPointerDown(event, [ fileIndex, pageIndex, drawnFieldIndex - ) + ]) } onPointerMove={(event) => { - handleDrawnFieldPointerMove(event, drawnField, page.width) + handleDrawnFieldPointerMove(event, page.width) }} + onPointerUp={handlePointerUpReleaseCapture} className={styles.drawingRectangle} style={{ backgroundColor: drawnField.counterpart @@ -472,12 +556,29 @@ export const DrawPDFFields = (props: Props) => { outlineColor: drawnField.counterpart ? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}` : undefined, - left: inPx(from(page.width, drawnField.left)), - top: inPx(from(page.width, drawnField.top)), - width: inPx(from(page.width, drawnField.width)), - height: inPx(from(page.width, drawnField.height)), + ...(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)), + top: inPx(from(page.width, drawnField.top)), + width: inPx(from(page.width, drawnField.width)), + height: inPx(from(page.width, drawnField.height)) + }), + pointerEvents: mouseState.clicked ? 'none' : 'all', touchAction: 'none', + zIndex: isActiveDrawnField( + fileIndex, + pageIndex, + drawnFieldIndex + ) + ? 60 + : undefined, opacity: mouseState.dragging && isActiveDrawnField( @@ -501,37 +602,36 @@ export const DrawPDFFields = (props: Props) => {
- handleResizePointerDown( - event, + handleResizePointerDown(event, [ fileIndex, pageIndex, drawnFieldIndex - ) + ]) } onPointerMove={(event) => { - handleResizePointerMove(event, drawnField, page.width) + handleResizePointerMove(event, page.width) }} + onPointerUp={handlePointerUpReleaseCapture} className={styles.resizeHandle} style={{ - background: - mouseState.resizing && + ...(mouseState.resizing && isActiveDrawnField( fileIndex, pageIndex, drawnFieldIndex - ) - ? 'var(--primary-main)' - : undefined + ) && { + cursor: 'grabbing', + opacity: 0.1 + }) }} > { - handleRemovePointerDown( - event, + handleRemovePointerDown(event, [ fileIndex, pageIndex, drawnFieldIndex - ) + ]) }} className={styles.removeHandle} > @@ -569,23 +669,26 @@ export const DrawPDFFields = (props: Props) => { onChange={(event) => { drawnField.counterpart = event.target.value setLastSigner(event.target.value) - refreshPdfFiles() }} labelId="counterparts" label="Counterparts" sx={{ background: 'white' }} - renderValue={(value) => - renderCounterpartValue(value) - } + renderValue={(value) => ( + + )} > {signers.map((signer, index) => { const npub = hexToNpub(signer.pubkey) - const metadata = props.metadata[signer.pubkey] + const profileMetadata = metadata[signer.pubkey] const displayValue = getProfileUsername( npub, - metadata + profileMetadata ) // make current signers dropdown visible if ( @@ -604,7 +707,7 @@ 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 ( -
- img': { - width: '21px', - height: '21px' - } - }} - /> - {displayValue} -
- ) - } - - return displayValue - } - return (
- {sigitFiles.map((file, i) => { - return ( - -
- {file.isPdf && getPdfPages(file, i)} - {file.isImage && ( - {file.name} - )} - {!(file.isPdf || file.isImage) && ( - - )} -
- {i < sigitFiles.length - 1 && } -
- ) - })} + {sigitFiles.length > 0 && + sigitFiles + .map((file, i) => + file.isPdf ? ( + + {getPdfPages(file, i)} + + ) : ( + + ) + ) + .reduce((prev, curr, i) => [ + prev, + , + curr + ])}
) } diff --git a/src/components/DrawPDFFields/internal/Counterpart.module.scss b/src/components/DrawPDFFields/internal/Counterpart.module.scss new file mode 100644 index 0000000..933913e --- /dev/null +++ b/src/components/DrawPDFFields/internal/Counterpart.module.scss @@ -0,0 +1,3 @@ +.counterpartSelectValue { + display: flex; +} diff --git a/src/components/DrawPDFFields/internal/Counterpart.tsx b/src/components/DrawPDFFields/internal/Counterpart.tsx new file mode 100644 index 0000000..1e2f61b --- /dev/null +++ b/src/components/DrawPDFFields/internal/Counterpart.tsx @@ -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 ( +
+ img': { + width: '21px', + height: '21px' + } + }} + /> + {displayValue} +
+ ) + } + + return displayValue + } +) diff --git a/src/components/DrawPDFFields/internal/FileItem.tsx b/src/components/DrawPDFFields/internal/FileItem.tsx new file mode 100644 index 0000000..a1f13ea --- /dev/null +++ b/src/components/DrawPDFFields/internal/FileItem.tsx @@ -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 = + if (file.isImage) return + + return ( +
+ {content} +
+ ) +}) diff --git a/src/components/DrawPDFFields/internal/ImageItem.tsx b/src/components/DrawPDFFields/internal/ImageItem.tsx new file mode 100644 index 0000000..8cba790 --- /dev/null +++ b/src/components/DrawPDFFields/internal/ImageItem.tsx @@ -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 {file.name} +}) diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 7ae0de3..5eb5307 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -47,7 +47,7 @@ background-color: #fff; border: 1px solid rgb(160, 160, 160); border-radius: 50%; - cursor: nwse-resize; + cursor: grab; // Increase the area a bit so it's easier to click &::after { @@ -85,10 +85,6 @@ } } -.counterpartSelectValue { - display: flex; -} - .counterpartAvatar { img { width: 21px; @@ -107,3 +103,16 @@ 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; +} diff --git a/src/components/LoadingSpinner/style.module.scss b/src/components/LoadingSpinner/style.module.scss index 088f389..2ed8093 100644 --- a/src/components/LoadingSpinner/style.module.scss +++ b/src/components/LoadingSpinner/style.module.scss @@ -7,7 +7,7 @@ align-items: center; justify-content: center; background-color: rgba(0, 0, 0, 0.5); - z-index: 50; + z-index: 70; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); } diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx index acd1874..95d577e 100644 --- a/src/components/PDFView/index.tsx +++ b/src/components/PDFView/index.tsx @@ -45,18 +45,17 @@ const PdfView = ({ const filterMarksByFile = (marks: Mark[], hash: string): Mark[] => { return marks.filter((mark) => mark.pdfFileHash === hash) } - const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean => - index !== files.length - 1 return (
{files.length > 0 ? ( - files.map((currentUserFile, index, arr) => { - const { hash, file, id } = currentUserFile + files + .map((currentUserFile) => { + const { hash, file, id } = currentUserFile - if (!hash) return - return ( - + if (!hash) return + return (
(pdfRefs.current[id] = el)} @@ -70,10 +69,13 @@ const PdfView = ({ otherUserMarks={filterMarksByFile(otherUserMarks, hash)} />
- {isNotLastPdfFile(index, arr) && } -
- ) - }) + ) + }) + .reduce((prev, curr, i) => [ + prev, + , + curr + ]) ) : ( )} diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 82f4d0c..f1ba4ad 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -79,10 +79,11 @@ import { } from '@fortawesome/free-solid-svg-icons' import { getSigitFile, SigitFile } from '../../utils/file.ts' import { generateTimestamp } from '../../utils/opentimestamps.ts' -import { Autocomplete } from '@mui/lab' +import { Autocomplete } from '@mui/material' import _, { truncate } from 'lodash' import * as React from 'react' import { AvatarIconButton } from '../../components/UserAvatarIconButton' +import { useImmer } from 'use-immer' type FoundUser = Event & { npub: string } @@ -98,7 +99,7 @@ export const CreatePage = () => { const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`) - const [selectedFiles, setSelectedFiles] = useState([]) + const [selectedFiles, setSelectedFiles] = useState([...uploadedFiles]) const fileInputRef = useRef(null) const handleUploadButtonClick = () => { if (fileInputRef.current) { @@ -123,7 +124,7 @@ export const CreatePage = () => { const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( {} ) - const [drawnFiles, setDrawnFiles] = useState([]) + const [drawnFiles, updateDrawnFiles] = useImmer([]) const [parsingPdf, setIsParsing] = useState(false) const searchFieldRef = useRef(null) @@ -295,8 +296,28 @@ export const CreatePage = () => { selectedFiles, 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) @@ -305,7 +326,7 @@ export const CreatePage = () => { setIsParsing(false) }) } - }, [selectedFiles]) + }, [selectedFiles, updateDrawnFiles]) /** * Changes the drawing tool @@ -357,12 +378,6 @@ export const CreatePage = () => { }) }, [metadata, users]) - useEffect(() => { - if (uploadedFiles) { - setSelectedFiles([...uploadedFiles]) - } - }, [uploadedFiles]) - useEffect(() => { if (usersPubkey) { 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) // Remove duplicates based on the file.name - setSelectedFiles((p) => - [...p, ...files].filter( + setSelectedFiles((p) => { + const unique = [...p, ...files].filter( (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() - setSelectedFiles((prevFiles) => - prevFiles.filter((file) => file.name !== fileToRemove.name) - ) + setSelectedFiles((prevFiles) => { + const files = prevFiles.filter((file) => file.name !== fileToRemove.name) + navigate('.', { + state: { uploadedFiles: files }, + replace: true + }) + return files + }) } // Validate inputs before proceeding @@ -1018,13 +1043,11 @@ export const CreatePage = () => { return JSON.parse(event.content) } catch (e) { return undefined - console.error(e) } } return ( <> - {isLoading && } { centerIcon={faFile} rightIcon={faToolbox} > - {parsingPdf ? ( - - ) : ( - - )} + + {parsingPdf && } + {isLoading && } ) }