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

Should SigitInfo returned by hook also contain other information available in Meta, specifically in the createSignature string, i.e. CreateSIgnatureEventContent? Similarly, should the data from docSignatures, represented by SignedEventContent be returned as well?

I beleive you would need to merge the current staging branch into yours to get a hold of some of these data types

Should `SigitInfo` returned by hook also contain other information available in `Meta`, specifically in the `createSignature` string, i.e. `CreateSIgnatureEventContent`? Similarly, should the data from `docSignatures`, represented by `SignedEventContent` be returned as well? I beleive you would need to merge the current staging branch into yours to get a hold of some of these data types
Outdated
Review

I've returned most of the things I found, we can add if something is missing later down the line 👍

I've returned most of the things I found, we can add if something is missing later down the line 👍
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)
}
}
}