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 { 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<SetStateAction<{ [key: string]: ProfileMetadata }>>
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<string>([
...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 (
<div className={styles.itemWrapper}>
<Link
to={
signedStatus === SignedStatus.Complete
signedStatus === SigitStatus.Complete
? appPublicRoutes.verify
: appPrivateRoutes.sign
}

View File

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

View File

@ -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<Event> {
// 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<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
})
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
/**
* 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<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>()
const [signers, setSigners] = useState<`npub1${string}`[]>([])
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 [signedStatus, setSignedStatus] = useState<SigitStatus>(
SigitStatus.Partial
)
const [fileExtensions, setFileExtensions] = useState<string[]>([])
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)
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.
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
}
}

View File

@ -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}
/>
))}
</div>

View File

@ -7,3 +7,4 @@ export * from './string'
export * from './zip'
export * from './utils'
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)
}
}
}