refactor(signature): apply strategy pattern and make it easier to expand with new tools
This commit is contained in:
parent
f72ad37ec0
commit
be146fa0fa
@ -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
|
||||
key={selectedMark.id}
|
||||
value={selectedMarkValue}
|
||||
placeholder={markLabel}
|
||||
handler={handleSelectedMarkValueChange}
|
||||
userMark={selectedMark}
|
||||
/>
|
||||
)}
|
||||
<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
|
||||
|
16
src/components/MarkTypeStrategy/MarkInput.tsx
Normal file
16
src/components/MarkTypeStrategy/MarkInput.tsx
Normal 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
|
||||
}
|
20
src/components/MarkTypeStrategy/MarkRender.tsx
Normal file
20
src/components/MarkTypeStrategy/MarkRender.tsx
Normal 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>
|
||||
)
|
32
src/components/MarkTypeStrategy/MarkStrategy.tsx
Normal file
32
src/components/MarkTypeStrategy/MarkStrategy.tsx
Normal 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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
@import '../../styles/colors.scss';
|
||||
@import '../../../styles/colors.scss';
|
||||
|
||||
$padding: 5px;
|
||||
|
@ -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,
|
@ -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>()
|
86
src/components/MarkTypeStrategy/Signature/index.tsx
Normal file
86
src/components/MarkTypeStrategy/Signature/index.tsx
Normal 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
|
||||
}
|
||||
}
|
@ -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,
|
7
src/components/MarkTypeStrategy/Text/index.tsx
Normal file
7
src/components/MarkTypeStrategy/Text/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { MarkStrategy } from '../MarkStrategy'
|
||||
import { MarkInputText } from './Input'
|
||||
|
||||
export const TextStrategy: MarkStrategy = {
|
||||
input: MarkInputText,
|
||||
render: ({ value }) => <>{value}</>
|
||||
}
|
@ -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
|
||||
key={getMarkValue()}
|
||||
value={getMarkValue()}
|
||||
mark={userMark.mark}
|
||||
/>
|
||||
)}
|
||||
<MarkRender
|
||||
key={getMarkValue()}
|
||||
markType={userMark.mark.type}
|
||||
value={getMarkValue()}
|
||||
mark={userMark.mark}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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`,
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user