From f896849ffd88d98c0edfae94eda2c6eed8b884ed Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 12 Aug 2024 14:26:03 +0200 Subject: [PATCH] refactor: expand useSigitMeta and add comments --- src/components/DisplaySigit/index.tsx | 75 +++++---- src/components/DisplaySigner/index.tsx | 2 + src/hooks/useSigitMeta.tsx | 222 ++++++++++++++----------- src/pages/home/index.tsx | 37 ++--- src/utils/index.ts | 1 + src/utils/meta.ts | 181 ++++++++++++++++++++ 6 files changed, 369 insertions(+), 149 deletions(-) create mode 100644 src/utils/meta.ts diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index e930b37..0fe5c2f 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -1,6 +1,6 @@ -import { Dispatch, SetStateAction, useEffect } from 'react' +import { useEffect, useState } from 'react' import { Meta, ProfileMetadata } from '../../types' -import { SigitInfo, SignedStatus } from '../../hooks/useSigitMeta' +import { SigitCardDisplayInfo, SigitStatus } from '../../utils' import { Event, kinds } from 'nostr-tools' import { Link } from 'react-router-dom' import { MetadataController } from '../../controllers' @@ -25,17 +25,10 @@ import { getExtensionIconLabel } from '../getExtensionIconLabel' type SigitProps = { meta: Meta - parsedMeta: SigitInfo - profiles: { [key: string]: ProfileMetadata } - setProfiles: Dispatch> + parsedMeta: SigitCardDisplayInfo } -export const DisplaySigit = ({ - meta, - parsedMeta, - profiles, - setProfiles -}: SigitProps) => { +export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { const { title, createdAt, @@ -45,51 +38,67 @@ export const DisplaySigit = ({ fileExtensions } = parsedMeta + const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>( + {} + ) + useEffect(() => { - const hexKeys: string[] = [] + const hexKeys = new Set([ + ...signers.map((signer) => npubToHex(signer)!) + ]) if (submittedBy) { - hexKeys.push(npubToHex(submittedBy)!) + hexKeys.add(npubToHex(submittedBy)!) } - hexKeys.push(...signers.map((signer) => npubToHex(signer)!)) const metadataController = new MetadataController() + + const handleMetadataEvent = (key: string) => (event: Event) => { + const metadataContent = + metadataController.extractProfileMetadataContent(event) + + if (metadataContent) { + setProfiles((prev) => ({ + ...prev, + [key]: metadataContent + })) + } + } + + const handleEventListener = + (key: string) => (kind: number, event: Event) => { + if (kind === kinds.Metadata) { + handleMetadataEvent(key)(event) + } + } + hexKeys.forEach((key) => { if (!(key in profiles)) { - const handleMetadataEvent = (event: Event) => { - const metadataContent = - metadataController.extractProfileMetadataContent(event) - - if (metadataContent) - setProfiles((prev) => ({ - ...prev, - [key]: metadataContent - })) - } - - metadataController.on(key, (kind: number, event: Event) => { - if (kind === kinds.Metadata) { - handleMetadataEvent(event) - } - }) + metadataController.on(key, handleEventListener(key)) metadataController .findMetadata(key) .then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(metadataEvent) + if (metadataEvent) handleMetadataEvent(key)(metadataEvent) }) .catch((err) => { console.error(`error occurred in finding metadata for: ${key}`, err) }) } }) - }, [submittedBy, signers, profiles, setProfiles]) + + return () => { + hexKeys.forEach((key) => { + metadataController.off(key, handleEventListener(key)) + }) + } + }, [submittedBy, signers, profiles]) return (
() useEffect(() => { + if (!meta) return + const updateSignStatus = async () => { const npub = hexToNpub(pubkey) if (npub in meta.docSignatures) { diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index e798ef0..d8ef9c2 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -1,117 +1,147 @@ import { useEffect, useState } from 'react' -import { toast } from 'react-toastify' import { CreateSignatureEventContent, Meta } from '../types' -import { parseJson } from '../utils' +import { Mark } from '../types/mark' +import { + parseCreateSignatureEvent, + parseCreateSignatureEventContent, + SigitMetaParseError, + SigitStatus, + SignStatus +} from '../utils' +import { toast } from 'react-toastify' +import { verifyEvent } from 'nostr-tools' import { Event } from 'nostr-tools' -type npub = `npub1${string}` +interface FlatMeta extends Meta, CreateSignatureEventContent, Partial { + // Validated create signature event + isValid: boolean -export enum SignedStatus { - Partial = 'In-Progress', - Complete = 'Completed' -} - -export interface SigitInfo { - createdAt?: number - title?: string - submittedBy?: string - signers: npub[] - fileExtensions: string[] - signedStatus: SignedStatus -} - -export const extractSigitInfo = async (meta: Meta) => { - if (!meta?.createSignature) return - - const sigitInfo: SigitInfo = { - signers: [], - fileExtensions: [], - signedStatus: SignedStatus.Partial + // Calculated status fields + signedStatus: SigitStatus + signersStatus: { + [signer: `npub1${string}`]: SignStatus } - - const createSignatureEvent = await parseJson( - meta.createSignature - ).catch((err) => { - console.log('err in parsing the createSignature event:>> ', err) - toast.error( - err.message || 'error occurred in parsing the create signature event' - ) - return - }) - - if (!createSignatureEvent) return - - // created_at in nostr events are stored in seconds - sigitInfo.createdAt = createSignatureEvent.created_at * 1000 - - const createSignatureContent = await parseJson( - createSignatureEvent.content - ).catch((err) => { - console.log(`err in parsing the createSignature event's content :>> `, err) - return - }) - - if (!createSignatureContent) return - - const files = Object.keys(createSignatureContent.fileHashes) - const extensions = files.reduce((result: string[], file: string) => { - const extension = file.split('.').pop() - if (extension) { - result.push(extension) - } - return result - }, []) - - const signedBy = Object.keys(meta.docSignatures) as npub[] - const isCompletelySigned = createSignatureContent.signers.every((signer) => - signedBy.includes(signer) - ) - - sigitInfo.title = createSignatureContent.title - sigitInfo.submittedBy = createSignatureEvent.pubkey - sigitInfo.signers = createSignatureContent.signers - sigitInfo.fileExtensions = extensions - - if (isCompletelySigned) { - sigitInfo.signedStatus = SignedStatus.Complete - } - - return sigitInfo } -export const useSigitMeta = (meta: Meta) => { - const [title, setTitle] = useState() - const [createdAt, setCreatedAt] = useState() - const [submittedBy, setSubmittedBy] = useState() - const [signers, setSigners] = useState([]) - const [signedStatus, setSignedStatus] = useState( - SignedStatus.Partial +/** + * Custom use hook for parsing the Sigit Meta + * @param meta Sigit Meta + * @returns flattened Meta object with calculated signed status + */ +export const useSigitMeta = (meta: Meta): FlatMeta => { + const [isValid, setIsValid] = useState(false) + const [kind, setKind] = useState() + const [tags, setTags] = useState() + const [created_at, setCreatedAt] = useState() + const [pubkey, setPubkey] = useState() // submittedBy, pubkey from nostr event + const [id, setId] = useState() + const [sig, setSig] = useState() + + const [signers, setSigners] = useState<`npub1${string}`[]>([]) + const [viewers, setViewers] = useState<`npub1${string}`[]>([]) + const [fileHashes, setFileHashes] = useState<{ + [user: `npub1${string}`]: string + }>({}) + const [markConfig, setMarkConfig] = useState([]) + const [title, setTitle] = useState('') + const [zipUrl, setZipUrl] = useState('') + + const [signedStatus, setSignedStatus] = useState( + SigitStatus.Partial ) - const [fileExtensions, setFileExtensions] = useState([]) + const [signersStatus, setSignersStatus] = useState<{ + [signer: `npub1${string}`]: SignStatus + }>({}) useEffect(() => { - const getSigitInfo = async () => { - const sigitInfo = await extractSigitInfo(meta) + if (!meta) return + ;(async function () { + try { + const createSignatureEvent = await parseCreateSignatureEvent( + meta.createSignature + ) - if (!sigitInfo) return + const { kind, tags, created_at, pubkey, id, sig, content } = + createSignatureEvent - setTitle(sigitInfo.title) - setCreatedAt(sigitInfo.createdAt) - setSubmittedBy(sigitInfo.submittedBy) - setSigners(sigitInfo.signers) - setSignedStatus(sigitInfo.signedStatus) - setFileExtensions(sigitInfo.fileExtensions) - } + setIsValid(verifyEvent(createSignatureEvent)) + setKind(kind) + setTags(tags) + // created_at in nostr events are stored in seconds + setCreatedAt(created_at * 1000) + setPubkey(pubkey) + setId(id) + setSig(sig) - getSigitInfo() + const { title, signers, viewers, fileHashes, markConfig, zipUrl } = + await parseCreateSignatureEventContent(content) + + setTitle(title) + setSigners(signers) + setViewers(viewers) + setFileHashes(fileHashes) + setMarkConfig(markConfig) + setZipUrl(zipUrl) + + // Parse each signature event and set signer status + for (const npub in meta.docSignatures) { + try { + const event = await parseCreateSignatureEvent( + meta.docSignatures[npub as `npub1${string}`] + ) + const isValidSignature = verifyEvent(event) + setSignersStatus((prev) => { + return { + ...prev, + [npub]: isValidSignature + ? SignStatus.Signed + : SignStatus.Invalid + } + }) + } catch (error) { + setSignersStatus((prev) => { + return { + ...prev, + [npub]: SignStatus.Invalid + } + }) + } + } + const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] + const isCompletelySigned = signers.every((signer) => + signedBy.includes(signer) + ) + setSignedStatus( + isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial + ) + } catch (error) { + if (error instanceof SigitMetaParseError) { + toast.error(error.message) + } + console.error(error) + } + })() }, [meta]) return { - title, - createdAt, - submittedBy, + modifiedAt: meta.modifiedAt, + createSignature: meta.createSignature, + docSignatures: meta.docSignatures, + keys: meta.keys, + isValid, + kind, + tags, + created_at, + pubkey, + id, + sig, signers, + viewers, + fileHashes, + markConfig, + title, + zipUrl, signedStatus, - fileExtensions + signersStatus } } diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index bb3115f..0f0329b 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -5,7 +5,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom' import { toast } from 'react-toastify' import { useAppSelector } from '../../hooks' import { appPrivateRoutes, appPublicRoutes } from '../../routes' -import { Meta, ProfileMetadata } from '../../types' +import { Meta } from '../../types' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSearch } from '@fortawesome/free-solid-svg-icons' import { Select } from '../../components/Select' @@ -14,10 +14,10 @@ import { useDropzone } from 'react-dropzone' import { Container } from '../../components/Container' import styles from './style.module.scss' import { - extractSigitInfo, - SigitInfo, - SignedStatus -} from '../../hooks/useSigitMeta' + extractSigitCardDisplayInfo, + SigitCardDisplayInfo, + SigitStatus +} from '../../utils' // Unsupported Filter options are commented const FILTERS = [ @@ -52,29 +52,28 @@ export const HomePage = () => { const [sigits, setSigits] = useState<{ [key: string]: Meta }>({}) const [parsedSigits, setParsedSigits] = useState<{ - [key: string]: SigitInfo + [key: string]: SigitCardDisplayInfo }>({}) - const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>( - {} - ) const usersAppData = useAppSelector((state) => state.userAppData) useEffect(() => { if (usersAppData) { const getSigitInfo = async () => { + const parsedSigits: { [key: string]: SigitCardDisplayInfo } = {} for (const key in usersAppData.sigits) { if (Object.prototype.hasOwnProperty.call(usersAppData.sigits, key)) { - const sigitInfo = await extractSigitInfo(usersAppData.sigits[key]) + const sigitInfo = await extractSigitCardDisplayInfo( + usersAppData.sigits[key] + ) if (sigitInfo) { - setParsedSigits((prev) => { - return { - ...prev, - [key]: sigitInfo - } - }) + parsedSigits[key] = sigitInfo } } } + + setParsedSigits({ + ...parsedSigits + }) } setSigits(usersAppData.sigits) @@ -240,9 +239,9 @@ export const HomePage = () => { const isMatch = title?.toLowerCase().includes(q.toLowerCase()) switch (filter) { case 'Completed': - return signedStatus === SignedStatus.Complete && isMatch + return signedStatus === SigitStatus.Complete && isMatch case 'In-progress': - return signedStatus === SignedStatus.Partial && isMatch + return signedStatus === SigitStatus.Partial && isMatch case 'Show all': return isMatch default: @@ -259,8 +258,6 @@ export const HomePage = () => { key={`sigit-${key}`} parsedMeta={parsedSigits[key]} meta={sigits[key]} - profiles={profiles} - setProfiles={setProfiles} /> ))}
diff --git a/src/utils/index.ts b/src/utils/index.ts index 1b0c133..ffac72d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,3 +7,4 @@ export * from './string' export * from './zip' export * from './utils' export * from './mark' +export * from './meta' diff --git a/src/utils/meta.ts b/src/utils/meta.ts new file mode 100644 index 0000000..e9e3f13 --- /dev/null +++ b/src/utils/meta.ts @@ -0,0 +1,181 @@ +import { CreateSignatureEventContent, Meta } from '../types' +import { parseJson } from '.' +import { Event } from 'nostr-tools' +import { toast } from 'react-toastify' + +export enum SignStatus { + Signed = 'Signed', + Pending = 'Pending', + Invalid = 'Invalid Sign' +} + +export enum SigitStatus { + Partial = 'In-Progress', + Complete = 'Completed' +} + +type Jsonable = + | string + | number + | boolean + | null + | undefined + | readonly Jsonable[] + | { readonly [key: string]: Jsonable } + | { toJSON(): Jsonable } + +export class SigitMetaParseError extends Error { + public readonly context?: Jsonable + + constructor( + message: string, + options: { cause?: Error; context?: Jsonable } = {} + ) { + const { cause, context } = options + + super(message, { cause }) + this.name = this.constructor.name + + this.context = context + } +} + +/** + * Handle meta errors + * Wraps the errors without message property and stringify to a message so we can use it later + * @param error + * @returns + */ +function handleError(error: unknown): Error { + if (error instanceof Error) return error + + // No message error, wrap it and stringify + let stringified = 'Unable to stringify the thrown value' + try { + stringified = JSON.stringify(error) + } catch (error) { + console.error(stringified, error) + } + + return new Error(`[SiGit Error]: ${stringified}`) +} + +// Reuse common error messages for meta parsing +export enum SigitMetaParseErrorType { + 'PARSE_ERROR_SIGNATURE_EVENT' = 'error occurred in parsing the create signature event', + 'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content" +} + +export interface SigitCardDisplayInfo { + createdAt?: number + title?: string + submittedBy?: string + signers: `npub1${string}`[] + fileExtensions: string[] + signedStatus: SigitStatus +} + +/** + * Wrapper for createSignatureEvent parse that throws custom SigitMetaParseError with cause and context + * @param raw Raw string for parsing + * @returns parsed Event + */ +export const parseCreateSignatureEvent = async ( + raw: string +): Promise => { + try { + const createSignatureEvent = await parseJson(raw) + return createSignatureEvent + } catch (error) { + throw new SigitMetaParseError( + SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT, + { + cause: handleError(error), + context: raw + } + ) + } +} + +/** + * Wrapper for event content parser that throws custom SigitMetaParseError with cause and context + * @param raw Raw string for parsing + * @returns parsed CreateSignatureEventContent + */ +export const parseCreateSignatureEventContent = async ( + raw: string +): Promise => { + try { + const createSignatureEventContent = + await parseJson(raw) + return createSignatureEventContent + } catch (error) { + throw new SigitMetaParseError( + SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT, + { + cause: handleError(error), + context: raw + } + ) + } +} + +/** + * Extracts only necessary metadata for the card display + * @param meta Sigit metadata + * @returns SigitCardDisplayInfo + */ +export const extractSigitCardDisplayInfo = async (meta: Meta) => { + if (!meta?.createSignature) return + + const sigitInfo: SigitCardDisplayInfo = { + signers: [], + fileExtensions: [], + signedStatus: SigitStatus.Partial + } + + try { + const createSignatureEvent = await parseCreateSignatureEvent( + meta.createSignature + ) + + // created_at in nostr events are stored in seconds + sigitInfo.createdAt = createSignatureEvent.created_at * 1000 + + const createSignatureContent = await parseCreateSignatureEventContent( + createSignatureEvent.content + ) + + const files = Object.keys(createSignatureContent.fileHashes) + const extensions = files.reduce((result: string[], file: string) => { + const extension = file.split('.').pop() + if (extension) { + result.push(extension) + } + return result + }, []) + + const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] + const isCompletelySigned = createSignatureContent.signers.every((signer) => + signedBy.includes(signer) + ) + + sigitInfo.title = createSignatureContent.title + sigitInfo.submittedBy = createSignatureEvent.pubkey + sigitInfo.signers = createSignatureContent.signers + sigitInfo.fileExtensions = extensions + + if (isCompletelySigned) { + sigitInfo.signedStatus = SigitStatus.Complete + } + + return sigitInfo + } catch (error) { + if (error instanceof SigitMetaParseError) { + toast.error(error.message) + console.error(error.name, error.message, error.cause, error.context) + } else { + console.error('Unexpected error', error) + } + } +}