feat(design): home page new design and functionality #135

Merged
enes merged 24 commits from issue-121 into staging 2024-08-14 08:44:09 +00:00
6 changed files with 369 additions and 149 deletions
Showing only changes of commit f896849ffd - Show all commits

View File

@ -1,6 +1,6 @@
import { Dispatch, SetStateAction, useEffect } from 'react' import { useEffect, useState } from 'react'
import { Meta, ProfileMetadata } from '../../types' import { Meta, ProfileMetadata } from '../../types'
import { SigitInfo, SignedStatus } from '../../hooks/useSigitMeta' import { SigitCardDisplayInfo, SigitStatus } from '../../utils'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { MetadataController } from '../../controllers' import { MetadataController } from '../../controllers'
@ -25,17 +25,10 @@ import { getExtensionIconLabel } from '../getExtensionIconLabel'
type SigitProps = { type SigitProps = {
meta: Meta meta: Meta
parsedMeta: SigitInfo parsedMeta: SigitCardDisplayInfo
profiles: { [key: string]: ProfileMetadata }
setProfiles: Dispatch<SetStateAction<{ [key: string]: ProfileMetadata }>>
} }
export const DisplaySigit = ({ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
meta,
parsedMeta,
profiles,
setProfiles
}: SigitProps) => {
const { const {
title, title,
createdAt, createdAt,
@ -45,51 +38,67 @@ export const DisplaySigit = ({
fileExtensions fileExtensions
} = parsedMeta } = parsedMeta
const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>(
{}
)
useEffect(() => { useEffect(() => {
const hexKeys: string[] = [] const hexKeys = new Set<string>([
...signers.map((signer) => npubToHex(signer)!)
])
if (submittedBy) { if (submittedBy) {
hexKeys.push(npubToHex(submittedBy)!) hexKeys.add(npubToHex(submittedBy)!)
} }
hexKeys.push(...signers.map((signer) => npubToHex(signer)!))
const metadataController = new MetadataController() const metadataController = new MetadataController()
hexKeys.forEach((key) => {
if (!(key in profiles)) { const handleMetadataEvent = (key: string) => (event: Event) => {
const handleMetadataEvent = (event: Event) => {
const metadataContent = const metadataContent =
metadataController.extractProfileMetadataContent(event) metadataController.extractProfileMetadataContent(event)
if (metadataContent) if (metadataContent) {
setProfiles((prev) => ({ setProfiles((prev) => ({
...prev, ...prev,
[key]: metadataContent [key]: metadataContent
})) }))
} }
metadataController.on(key, (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(event)
} }
})
const handleEventListener =
(key: string) => (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(key)(event)
}
}
hexKeys.forEach((key) => {
if (!(key in profiles)) {
metadataController.on(key, handleEventListener(key))
metadataController metadataController
.findMetadata(key) .findMetadata(key)
.then((metadataEvent) => { .then((metadataEvent) => {
if (metadataEvent) handleMetadataEvent(metadataEvent) if (metadataEvent) handleMetadataEvent(key)(metadataEvent)
}) })
.catch((err) => { .catch((err) => {
console.error(`error occurred in finding metadata for: ${key}`, 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 ( return (
<div className={styles.itemWrapper}> <div className={styles.itemWrapper}>
<Link <Link
to={ to={
signedStatus === SignedStatus.Complete signedStatus === SigitStatus.Complete
? appPublicRoutes.verify ? appPublicRoutes.verify
: appPrivateRoutes.sign : appPrivateRoutes.sign
} }

View File

@ -28,6 +28,8 @@ export const DisplaySigner = ({
const [signStatus, setSignedStatus] = useState<SignStatus>() const [signStatus, setSignedStatus] = useState<SignStatus>()
useEffect(() => { useEffect(() => {
if (!meta) return
const updateSignStatus = async () => { const updateSignStatus = async () => {
const npub = hexToNpub(pubkey) const npub = hexToNpub(pubkey)
if (npub in meta.docSignatures) { if (npub in meta.docSignatures) {

View File

@ -1,117 +1,147 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import { CreateSignatureEventContent, Meta } from '../types' 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' import { Event } from 'nostr-tools'
type npub = `npub1${string}` interface FlatMeta extends Meta, CreateSignatureEventContent, Partial<Event> {
// Validated create signature event
isValid: boolean
export enum SignedStatus { // Calculated status fields
Partial = 'In-Progress', signedStatus: SigitStatus
Complete = 'Completed' signersStatus: {
[signer: `npub1${string}`]: SignStatus
}
} }
export interface SigitInfo { /**
createdAt?: number * Custom use hook for parsing the Sigit Meta
title?: string * @param meta Sigit Meta
submittedBy?: string * @returns flattened Meta object with calculated signed status
signers: npub[] */
fileExtensions: string[] export const useSigitMeta = (meta: Meta): FlatMeta => {
signedStatus: SignedStatus const [isValid, setIsValid] = useState(false)
} const [kind, setKind] = useState<number>()
const [tags, setTags] = useState<string[][]>()
const [created_at, setCreatedAt] = useState<number>()
const [pubkey, setPubkey] = useState<string>() // submittedBy, pubkey from nostr event
const [id, setId] = useState<string>()
const [sig, setSig] = useState<string>()
export const extractSigitInfo = async (meta: Meta) => { const [signers, setSigners] = useState<`npub1${string}`[]>([])
if (!meta?.createSignature) return const [viewers, setViewers] = useState<`npub1${string}`[]>([])
const [fileHashes, setFileHashes] = useState<{
[user: `npub1${string}`]: string
}>({})
const [markConfig, setMarkConfig] = useState<Mark[]>([])
const [title, setTitle] = useState<string>('')
const [zipUrl, setZipUrl] = useState<string>('')
const sigitInfo: SigitInfo = { const [signedStatus, setSignedStatus] = useState<SigitStatus>(
signers: [], SigitStatus.Partial
fileExtensions: [],
signedStatus: SignedStatus.Partial
}
const createSignatureEvent = await parseJson<Event>(
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 const [signersStatus, setSignersStatus] = useState<{
}) [signer: `npub1${string}`]: SignStatus
}>({})
if (!createSignatureEvent) return
// created_at in nostr events are stored in seconds
sigitInfo.createdAt = createSignatureEvent.created_at * 1000
const createSignatureContent = await parseJson<CreateSignatureEventContent>(
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<string>()
const [createdAt, setCreatedAt] = useState<number>()
const [submittedBy, setSubmittedBy] = useState<string>()
const [signers, setSigners] = useState<npub[]>([])
const [signedStatus, setSignedStatus] = useState<SignedStatus>(
SignedStatus.Partial
)
const [fileExtensions, setFileExtensions] = useState<string[]>([])
useEffect(() => { useEffect(() => {
const getSigitInfo = async () => { if (!meta) return
const sigitInfo = await extractSigitInfo(meta) ;(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) setIsValid(verifyEvent(createSignatureEvent))
setCreatedAt(sigitInfo.createdAt) setKind(kind)
setSubmittedBy(sigitInfo.submittedBy) setTags(tags)
setSigners(sigitInfo.signers) // created_at in nostr events are stored in seconds
setSignedStatus(sigitInfo.signedStatus) setCreatedAt(created_at * 1000)
Review

created_at * 1000 should be made into a utility function and used in other similar places (there is at least one that I could find).

Similarly, there is a reverse action, created_at / 1000, which should also be a utility.

`created_at * 1000` should be made into a utility function and used in other similar places (there is at least one that I could find). Similarly, there is a reverse action, `created_at / 1000`, which should also be a utility.
setFileExtensions(sigitInfo.fileExtensions) setPubkey(pubkey)
setId(id)
setSig(sig)
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
} }
})
getSigitInfo() } 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]) }, [meta])
return { return {
title, modifiedAt: meta.modifiedAt,
createdAt, createSignature: meta.createSignature,
submittedBy, docSignatures: meta.docSignatures,
keys: meta.keys,
isValid,
kind,
tags,
created_at,
pubkey,
id,
sig,
signers, signers,
viewers,
fileHashes,
markConfig,
title,
zipUrl,
signedStatus, signedStatus,
fileExtensions signersStatus
} }
} }

View File

@ -5,7 +5,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { useAppSelector } from '../../hooks' import { useAppSelector } from '../../hooks'
import { appPrivateRoutes, appPublicRoutes } from '../../routes' import { appPrivateRoutes, appPublicRoutes } from '../../routes'
import { Meta, ProfileMetadata } from '../../types' import { Meta } from '../../types'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch } from '@fortawesome/free-solid-svg-icons' import { faSearch } from '@fortawesome/free-solid-svg-icons'
import { Select } from '../../components/Select' import { Select } from '../../components/Select'
@ -14,10 +14,10 @@ import { useDropzone } from 'react-dropzone'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import styles from './style.module.scss' import styles from './style.module.scss'
import { import {
extractSigitInfo, extractSigitCardDisplayInfo,
SigitInfo, SigitCardDisplayInfo,
SignedStatus SigitStatus
} from '../../hooks/useSigitMeta' } from '../../utils'
// Unsupported Filter options are commented // Unsupported Filter options are commented
const FILTERS = [ const FILTERS = [
@ -52,30 +52,29 @@ export const HomePage = () => {
const [sigits, setSigits] = useState<{ [key: string]: Meta }>({}) const [sigits, setSigits] = useState<{ [key: string]: Meta }>({})
const [parsedSigits, setParsedSigits] = useState<{ const [parsedSigits, setParsedSigits] = useState<{
[key: string]: SigitInfo [key: string]: SigitCardDisplayInfo
}>({}) }>({})
const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>(
{}
)
const usersAppData = useAppSelector((state) => state.userAppData) const usersAppData = useAppSelector((state) => state.userAppData)
useEffect(() => { useEffect(() => {
if (usersAppData) { if (usersAppData) {
const getSigitInfo = async () => { const getSigitInfo = async () => {
const parsedSigits: { [key: string]: SigitCardDisplayInfo } = {}
for (const key in usersAppData.sigits) { for (const key in usersAppData.sigits) {
if (Object.prototype.hasOwnProperty.call(usersAppData.sigits, key)) { if (Object.prototype.hasOwnProperty.call(usersAppData.sigits, key)) {
const sigitInfo = await extractSigitInfo(usersAppData.sigits[key]) const sigitInfo = await extractSigitCardDisplayInfo(
usersAppData.sigits[key]
)
if (sigitInfo) { if (sigitInfo) {
setParsedSigits((prev) => { parsedSigits[key] = sigitInfo
return {
...prev,
[key]: sigitInfo
} }
}
}
setParsedSigits({
...parsedSigits
}) })
} }
}
}
}
setSigits(usersAppData.sigits) setSigits(usersAppData.sigits)
getSigitInfo() getSigitInfo()
@ -240,9 +239,9 @@ export const HomePage = () => {
const isMatch = title?.toLowerCase().includes(q.toLowerCase()) const isMatch = title?.toLowerCase().includes(q.toLowerCase())
switch (filter) { switch (filter) {
case 'Completed': case 'Completed':
return signedStatus === SignedStatus.Complete && isMatch return signedStatus === SigitStatus.Complete && isMatch
case 'In-progress': case 'In-progress':
return signedStatus === SignedStatus.Partial && isMatch return signedStatus === SigitStatus.Partial && isMatch
case 'Show all': case 'Show all':
return isMatch return isMatch
default: default:
@ -259,8 +258,6 @@ export const HomePage = () => {
key={`sigit-${key}`} key={`sigit-${key}`}
parsedMeta={parsedSigits[key]} parsedMeta={parsedSigits[key]}
meta={sigits[key]} meta={sigits[key]}
profiles={profiles}
setProfiles={setProfiles}
/> />
))} ))}
</div> </div>

View File

@ -7,3 +7,4 @@ export * from './string'
export * from './zip' export * from './zip'
export * from './utils' export * from './utils'
export * from './mark' export * from './mark'
export * from './meta'

181
src/utils/meta.ts Normal file
View File

@ -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<Event> => {
try {
const createSignatureEvent = await parseJson<Event>(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<CreateSignatureEventContent> => {
try {
const createSignatureEventContent =
await parseJson<CreateSignatureEventContent>(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)
}
}
}