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

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 👍
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)
}
}
}