refactor(signature): apply strategy pattern and make it easier to expand with new tools

This commit is contained in:
enes 2024-11-18 17:20:20 +01:00
parent f72ad37ec0
commit be146fa0fa
20 changed files with 200 additions and 162 deletions

View File

@ -7,7 +7,7 @@ import {
isCurrentValueLast isCurrentValueLast
} from '../../utils' } from '../../utils'
import React, { useState } from 'react' import React, { useState } from 'react'
import { MARK_TYPE_CONFIG } from '../getMarkComponents.tsx' import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
interface MarkFormFieldProps { interface MarkFormFieldProps {
currentUserMarks: CurrentUserMark[] currentUserMarks: CurrentUserMark[]
@ -52,8 +52,7 @@ 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}>
@ -84,15 +83,14 @@ const MarkFormField = ({
</div> </div>
<div className={styles.inputWrapper}> <div className={styles.inputWrapper}>
<form onSubmit={(e) => handleFormSubmit(e)}> <form onSubmit={(e) => handleFormSubmit(e)}>
{typeof MarkInputComponent !== 'undefined' && ( <MarkInput
<MarkInputComponent markType={selectedMark.mark.type}
key={selectedMark.id} key={selectedMark.id}
value={selectedMarkValue} value={selectedMarkValue}
placeholder={markLabel} placeholder={markLabel}
handler={handleSelectedMarkValueChange} handler={handleSelectedMarkValueChange}
userMark={selectedMark} userMark={selectedMark}
/> />
)}
<div className={styles.actionsBottom}> <div className={styles.actionsBottom}>
<button type="submit" className={styles.submitButton}> <button type="submit" className={styles.submitButton}>
NEXT NEXT

View File

@ -0,0 +1,16 @@
import { MarkType } from '../../types/drawing'
import { MARK_TYPE_CONFIG, MarkInputProps } from './MarkStrategy'
interface MarkInputComponentProps extends MarkInputProps {
markType: MarkType
}
export const MarkInput = ({ markType, ...rest }: MarkInputComponentProps) => {
const { input: InputComponent } = MARK_TYPE_CONFIG[markType] || {}
if (typeof InputComponent !== 'undefined') {
return <InputComponent {...rest} />
}
return null
}

View File

@ -0,0 +1,20 @@
import { MarkType } from '../../types/drawing'
import { MARK_TYPE_CONFIG, MarkRenderProps } from './MarkStrategy'
interface MarkRenderComponentProps extends MarkRenderProps {
markType: MarkType
}
export const MarkRender = ({ markType, ...rest }: MarkRenderComponentProps) => {
const { render: RenderComponent } = MARK_TYPE_CONFIG[markType] || {}
if (typeof RenderComponent !== 'undefined') {
return <RenderComponent {...rest} />
}
return <DefaultRenderComponent {...rest} />
}
const DefaultRenderComponent = ({ value }: MarkRenderProps) => (
<span>{value}</span>
)

View File

@ -0,0 +1,32 @@
import { MarkType } from '../../types/drawing'
import { CurrentUserMark, Mark } from '../../types/mark'
import { TextStrategy } from './Text'
import { SignatureStrategy } from './Signature'
export interface MarkInputProps {
value: string
handler: (value: string) => void
placeholder?: string
userMark?: CurrentUserMark
}
export interface MarkRenderProps {
value?: string
mark: Mark
}
export interface MarkStrategy {
input: React.FC<MarkInputProps>
render: React.FC<MarkRenderProps>
encryptAndUpload?: (value: string, key?: string) => Promise<string>
fetchAndDecrypt?: (value: string, key?: string) => Promise<string>
}
export type MarkStrategies = {
[key in MarkType]?: MarkStrategy
}
export const MARK_TYPE_CONFIG: MarkStrategies = {
[MarkType.TEXT]: TextStrategy,
[MarkType.SIGNATURE]: SignatureStrategy
}

View File

@ -1,4 +1,4 @@
@import '../../styles/colors.scss'; @import '../../../styles/colors.scss';
$padding: 5px; $padding: 5px;

View File

@ -1,12 +1,12 @@
import { useCallback, useEffect, useRef } from 'react' import { useCallback, useEffect, useRef } from 'react'
import { MarkInputProps } from '../../types/mark'
import { faEraser } from '@fortawesome/free-solid-svg-icons' import { faEraser } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import styles from './Signature.module.scss' import { MarkRenderSignature } from './Render'
import { MarkRenderSignature } from '../MarkRender/Signature'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../utils/const' import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../../utils/const'
import { BasicPoint } from 'signature_pad/dist/types/point' import { BasicPoint } from 'signature_pad/dist/types/point'
import { MarkInputProps } from '../MarkStrategy'
import styles from './Input.module.scss'
export const MarkInputSignature = ({ export const MarkInputSignature = ({
value, value,

View File

@ -1,9 +1,9 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import { MarkRenderProps } from '../../types/mark' import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../../utils'
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../utils'
import { BasicPoint } from 'signature_pad/dist/types/point' import { BasicPoint } from 'signature_pad/dist/types/point'
import styles from './Signature.module.scss' import { MarkRenderProps } from '../MarkStrategy'
import styles from './Render.module.scss'
export const MarkRenderSignature = ({ value }: MarkRenderProps) => { export const MarkRenderSignature = ({ value }: MarkRenderProps) => {
const [dataUrl, setDataUrl] = useState<string | undefined>() const [dataUrl, setDataUrl] = useState<string | undefined>()

View File

@ -0,0 +1,86 @@
import axios from 'axios'
import {
decryptArrayBuffer,
encryptArrayBuffer,
getHash,
isOnline,
uploadToFileStorage
} from '../../../utils'
import { MarkStrategy } from '../MarkStrategy'
import { MarkInputSignature } from './Input'
import { MarkRenderSignature } from './Render'
export const SignatureStrategy: MarkStrategy = {
input: MarkInputSignature,
render: MarkRenderSignature,
encryptAndUpload: async (value, encryptionKey) => {
// Value is the stringified signature object
// Encode it as text to the arrayBuffer
const encoder = new TextEncoder()
const uint8Array = encoder.encode(value)
const hash = await getHash(uint8Array)
if (!hash) {
throw new Error("Can't get file hash.")
}
if (!encryptionKey) {
throw new Error('Signature requires an encryption key')
}
// Encrypt the file contents with the same encryption key from the create signature
const encryptedArrayBuffer = await encryptArrayBuffer(
uint8Array,
encryptionKey
)
// Create the encrypted json file from array buffer and hash
const file = new File([encryptedArrayBuffer], `${hash}.json`)
if (await isOnline()) {
try {
const url = await uploadToFileStorage(file)
console.info(`${file.name} uploaded to file storage`)
return url
} catch (error) {
if (error instanceof Error) {
console.error(
`Error occurred in uploading file ${file.name}`,
error.message
)
}
}
} else {
// Handle offline?
}
return value
},
fetchAndDecrypt: async (value, encryptionKey) => {
if (!encryptionKey) {
throw new Error('Signature requires an encryption key')
}
const encryptedArrayBuffer = await axios.get(value, {
responseType: 'arraybuffer'
})
const arrayBuffer = await decryptArrayBuffer(
encryptedArrayBuffer.data,
encryptionKey
).catch((err) => {
console.log('err in decryption:>> ', err)
return null
})
if (arrayBuffer) {
// decode json
const decoder = new TextDecoder()
const value = decoder.decode(arrayBuffer)
return value
}
// Handle offline?
return value
}
}

View File

@ -1,5 +1,5 @@
import { MarkInputProps } from '../../types/mark' import { MarkInputProps } from '../MarkStrategy'
import styles from '../MarkFormField/style.module.scss' import styles from '../../MarkFormField/style.module.scss'
export const MarkInputText = ({ export const MarkInputText = ({
value, value,

View File

@ -0,0 +1,7 @@
import { MarkStrategy } from '../MarkStrategy'
import { MarkInputText } from './Input'
export const TextStrategy: MarkStrategy = {
input: MarkInputText,
render: ({ value }) => <>{value}</>
}

View File

@ -4,7 +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' import { MarkRender } from '../MarkTypeStrategy/MarkRender.tsx'
interface PdfMarkItemProps { interface PdfMarkItemProps {
userMark: CurrentUserMark userMark: CurrentUserMark
@ -28,8 +28,6 @@ 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}
@ -50,13 +48,12 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
fontSize: inPx(from(pageWidth, FONT_SIZE)) fontSize: inPx(from(pageWidth, FONT_SIZE))
}} }}
> >
{typeof MarkRenderComponent !== 'undefined' && ( <MarkRender
<MarkRenderComponent key={getMarkValue()}
key={getMarkValue()} markType={userMark.mark.type}
value={getMarkValue()} value={getMarkValue()}
mark={userMark.mark} mark={userMark.mark}
/> />
)}
</div> </div>
) )
} }

View File

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

View File

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

View File

@ -21,7 +21,7 @@ import { Event } from 'nostr-tools'
import store from '../store/store' import store from '../store/store'
import { NostrController } from '../controllers' import { NostrController } from '../controllers'
import { MetaParseError } from '../types/errors/MetaParseError' import { MetaParseError } from '../types/errors/MetaParseError'
import { MARK_TYPE_CONFIG } from '../components/getMarkComponents' import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy'
/** /**
* Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`, * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,

View File

@ -55,7 +55,7 @@ import {
} from '../../utils/file.ts' } from '../../utils/file.ts'
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts' import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
import { generateTimestamp } from '../../utils/opentimestamps.ts' import { generateTimestamp } from '../../utils/opentimestamps.ts'
import { MARK_TYPE_CONFIG } from '../../components/getMarkComponents.tsx' import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
enum SignedStatus { enum SignedStatus {
Fully_Signed, Fully_Signed,

View File

@ -55,7 +55,7 @@ import {
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts' import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
import _ from 'lodash' import _ from 'lodash'
import { MARK_TYPE_CONFIG } from '../../components/getMarkComponents.tsx' import { MarkRender } from '../../components/MarkTypeStrategy/MarkRender.tsx'
interface PdfViewProps { interface PdfViewProps {
files: CurrentUserFile[] files: CurrentUserFile[]
@ -115,8 +115,6 @@ 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}`}
@ -132,9 +130,11 @@ const SlimPdfView = ({
fontSize: inPx(from(page.width, FONT_SIZE)) fontSize: inPx(from(page.width, FONT_SIZE))
}} }}
> >
{typeof MarkRenderComponent !== 'undefined' && ( <MarkRender
<MarkRenderComponent value={m.value} mark={m} /> markType={m.type}
)} value={m.value}
mark={m}
/>
</div> </div>
) )
})} })}

View File

@ -28,26 +28,3 @@ 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>
encryptAndUpload?: (value: string, key?: string) => Promise<string>
fetchAndDecrypt?: (value: string, key?: string) => Promise<string>
}
export type MarkConfigs = {
[key in MarkType]?: MarkConfig
}

View File

@ -1,4 +1,4 @@
import { MARK_TYPE_CONFIG } from '../components/getMarkComponents.tsx' import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy.tsx'
import { NostrController } from '../controllers/NostrController.ts' import { NostrController } from '../controllers/NostrController.ts'
import store from '../store/store.ts' import store from '../store/store.ts'
import { Meta } from '../types' import { Meta } from '../types'

View File

@ -24,7 +24,7 @@ import {
faStamp, faStamp,
faTableCellsLarge faTableCellsLarge
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { MARK_TYPE_CONFIG } from '../components/getMarkComponents.tsx' import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy.tsx'
/** /**
* Takes in an array of Marks already filtered by User. * Takes in an array of Marks already filtered by User.