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

View File

@ -1,12 +1,12 @@
import { useCallback, useEffect, useRef } from 'react'
import { MarkInputProps } from '../../types/mark'
import { faEraser } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import styles from './Signature.module.scss'
import { MarkRenderSignature } from '../MarkRender/Signature'
import { MarkRenderSignature } from './Render'
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 { MarkInputProps } from '../MarkStrategy'
import styles from './Input.module.scss'
export const MarkInputSignature = ({
value,

View File

@ -1,9 +1,9 @@
import { useEffect, useState } from 'react'
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 styles from './Signature.module.scss'
import { MarkRenderProps } from '../MarkStrategy'
import styles from './Render.module.scss'
export const MarkRenderSignature = ({ value }: MarkRenderProps) => {
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 styles from '../MarkFormField/style.module.scss'
import { MarkInputProps } from '../MarkStrategy'
import styles from '../../MarkFormField/style.module.scss'
export const MarkInputText = ({
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 { forwardRef } from 'react'
import { npubToHex } from '../../utils/nostr.ts'
import { MARK_TYPE_CONFIG } from '../getMarkComponents.tsx'
import { MarkRender } from '../MarkTypeStrategy/MarkRender.tsx'
interface PdfMarkItemProps {
userMark: CurrentUserMark
@ -28,8 +28,6 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
const getMarkValue = () =>
isEdited() ? selectedMarkValue : userMark.currentValue
const { from } = useScale()
const { render: MarkRenderComponent } =
MARK_TYPE_CONFIG[userMark.mark.type] || {}
return (
<div
ref={ref}
@ -50,13 +48,12 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
fontSize: inPx(from(pageWidth, FONT_SIZE))
}}
>
{typeof MarkRenderComponent !== 'undefined' && (
<MarkRenderComponent
<MarkRender
key={getMarkValue()}
markType={userMark.mark.type}
value={getMarkValue()}
mark={userMark.mark}
/>
)}
</div>
)
}

View File

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

View File

@ -55,7 +55,7 @@ import {
} from '../../utils/file.ts'
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.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 {
Fully_Signed,

View File

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

View File

@ -28,26 +28,3 @@ export interface MarkRect {
width: 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 store from '../store/store.ts'
import { Meta } from '../types'

View File

@ -24,7 +24,7 @@ import {
faStamp,
faTableCellsLarge
} 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.