refactor: useSigitMeta, DisplaySigners and UserDetails section

This commit is contained in:
enes 2024-08-14 14:27:49 +02:00
parent 01ca81be2a
commit 4b04bdf39e
8 changed files with 384 additions and 330 deletions

View File

@ -20,6 +20,7 @@ import styles from './style.module.scss'
import { TooltipChild } from '../TooltipChild'
import { getExtensionIconLabel } from '../getExtensionIconLabel'
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
import { useSigitMeta } from '../../hooks/useSigitMeta'
type SigitProps = {
meta: Meta
@ -36,6 +37,8 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
fileExtensions
} = parsedMeta
const { signersStatus } = useSigitMeta(meta)
const profiles = useSigitProfiles([
...(submittedBy ? [submittedBy] : []),
...signers
@ -94,7 +97,7 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
>
<TooltipChild>
<DisplaySigner
meta={meta}
status={signersStatus[signer]}
profile={profile}
pubkey={pubkey}
/>

View File

@ -1,58 +1,50 @@
import { Badge } from '@mui/material'
import { Event, verifyEvent } from 'nostr-tools'
import { useState, useEffect } from 'react'
import { Meta, ProfileMetadata } from '../../types'
import { hexToNpub, parseJson } from '../../utils'
import { ProfileMetadata } from '../../types'
import styles from './style.module.scss'
import { UserAvatar } from '../UserAvatar'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheck, faExclamation } from '@fortawesome/free-solid-svg-icons'
enum SignStatus {
Signed = 'Signed',
Pending = 'Pending',
Invalid = 'Invalid Sign'
}
import {
faCheck,
faExclamation,
faEye,
faHourglass,
faQuestion
} from '@fortawesome/free-solid-svg-icons'
import { SignStatus } from '../../utils'
import { Spinner } from '../Spinner'
type DisplaySignerProps = {
meta: Meta
profile: ProfileMetadata
pubkey: string
status: SignStatus
}
export const DisplaySigner = ({
meta,
status,
profile,
pubkey
}: DisplaySignerProps) => {
const [signStatus, setSignedStatus] = useState<SignStatus>()
const getStatusIcon = (status: SignStatus) => {
switch (status) {
case SignStatus.Signed:
return <FontAwesomeIcon icon={faCheck} />
case SignStatus.Awaiting:
return (
<Spinner>
<FontAwesomeIcon icon={faHourglass} />
</Spinner>
)
case SignStatus.Pending:
return <FontAwesomeIcon icon={faHourglass} />
case SignStatus.Invalid:
return <FontAwesomeIcon icon={faExclamation} />
case SignStatus.Viewer:
return <FontAwesomeIcon icon={faEye} />
useEffect(() => {
if (!meta) return
const updateSignStatus = async () => {
const npub = hexToNpub(pubkey)
if (npub in meta.docSignatures) {
parseJson<Event>(meta.docSignatures[npub])
.then((event) => {
const isValidSignature = verifyEvent(event)
if (isValidSignature) {
setSignedStatus(SignStatus.Signed)
} else {
setSignedStatus(SignStatus.Invalid)
}
})
.catch((err) => {
console.log(`err in parsing the docSignatures for ${npub}:>> `, err)
setSignedStatus(SignStatus.Invalid)
})
} else {
setSignedStatus(SignStatus.Pending)
}
default:
return <FontAwesomeIcon icon={faQuestion} />
}
updateSignStatus()
}, [meta, pubkey])
}
return (
<Badge
@ -60,16 +52,7 @@ export const DisplaySigner = ({
overlap="circular"
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
badgeContent={
signStatus !== SignStatus.Pending && (
<div className={styles.statusBadge}>
{signStatus === SignStatus.Signed && (
<FontAwesomeIcon icon={faCheck} />
)}
{signStatus === SignStatus.Invalid && (
<FontAwesomeIcon icon={faExclamation} />
)}
</div>
)
<div className={styles.statusBadge}>{getStatusIcon(status)}</div>
}
>
<UserAvatar pubkey={pubkey} image={profile?.picture} />

View File

@ -1,246 +0,0 @@
import { CheckCircle, Cancel } from '@mui/icons-material'
import { Divider, Tooltip } from '@mui/material'
import { verifyEvent } from 'nostr-tools'
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
import { Meta, SignedEventContent } from '../../types'
import {
extractFileExtensions,
formatTimestamp,
fromUnixTimestamp,
hexToNpub,
npubToHex,
shorten
} from '../../utils'
import { UserAvatar } from '../UserAvatar'
import { useSigitMeta } from '../../hooks/useSigitMeta'
import { UserAvatarGroup } from '../UserAvatarGroup'
import styles from './style.module.scss'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faCalendar,
faCalendarCheck,
faCalendarPlus,
faEye,
faFile,
faFileCircleExclamation
} from '@fortawesome/free-solid-svg-icons'
import { getExtensionIconLabel } from '../getExtensionIconLabel'
import { useSelector } from 'react-redux'
import { State } from '../../store/rootReducer'
interface FileUsersProps {
meta: Meta
}
export const FileUsers = ({ meta }: FileUsersProps) => {
const { usersPubkey } = useSelector((state: State) => state.auth)
const {
submittedBy,
signers,
viewers,
fileHashes,
sig,
docSignatures,
parsedSignatureEvents,
createdAt,
signedStatus,
completedAt
} = useSigitMeta(meta)
const profiles = useSigitProfiles([
...(submittedBy ? [submittedBy] : []),
...signers,
...viewers
])
const userCanSign =
typeof usersPubkey !== 'undefined' &&
signers.includes(hexToNpub(usersPubkey))
const ext = extractFileExtensions(Object.keys(fileHashes))
const getPrevSignersSig = (npub: string) => {
// if user is first signer then use creator's signature
if (signers[0] === npub) {
return sig
}
// find the index of signer
const currentSignerIndex = signers.findIndex((signer) => signer === npub)
// return null if could not found user in signer's list
if (currentSignerIndex === -1) return null
// find prev signer
const prevSigner = signers[currentSignerIndex - 1]
// get the signature of prev signer
try {
const prevSignersEvent = parsedSignatureEvents[prevSigner]
return prevSignersEvent.sig
} catch (error) {
return null
}
}
const displayUser = (pubkey: string, verifySignature = false) => {
const profile = profiles[pubkey]
let isValidSignature = false
if (verifySignature) {
const npub = hexToNpub(pubkey)
const signedEventString = docSignatures[npub]
if (signedEventString) {
try {
const signedEvent = JSON.parse(signedEventString)
const isVerifiedEvent = verifyEvent(signedEvent)
if (isVerifiedEvent) {
// get the actual signature of prev signer
const prevSignersSig = getPrevSignersSig(npub)
// get the signature of prev signer from the content of current signers signedEvent
try {
const obj: SignedEventContent = JSON.parse(signedEvent.content)
if (
obj.prevSig &&
prevSignersSig &&
obj.prevSig === prevSignersSig
) {
isValidSignature = true
}
} catch (error) {
isValidSignature = false
}
}
} catch (error) {
console.error(
`An error occurred in parsing and verifying the signature event for ${pubkey}`,
error
)
}
}
}
return (
<>
<UserAvatar
pubkey={pubkey}
name={
profile?.display_name || profile?.name || shorten(hexToNpub(pubkey))
}
image={profile?.picture}
/>
{verifySignature && (
<>
{isValidSignature && (
<Tooltip title="Valid signature">
<CheckCircle sx={{ color: 'green' }} />
</Tooltip>
)}
{!isValidSignature && (
<Tooltip title="Invalid signature">
<Cancel sx={{ color: 'red' }} />
</Tooltip>
)}
</>
)}
</>
)
}
return submittedBy ? (
<div className={styles.container}>
<div className={styles.section}>
<p>Signers</p>
{displayUser(submittedBy)}
{submittedBy && signers.length ? (
<Divider orientation="vertical" flexItem />
) : null}
<UserAvatarGroup max={7}>
{signers.length > 0 &&
signers.map((signer) => (
<span key={signer}>{displayUser(npubToHex(signer)!, true)}</span>
))}
{viewers.length > 0 &&
viewers.map((viewer) => (
<span key={viewer}>{displayUser(npubToHex(viewer)!)}</span>
))}
</UserAvatarGroup>
</div>
<div className={styles.section}>
<p>Details</p>
<Tooltip
title={'Publication date'}
placement="top"
arrow
disableInteractive
>
<span className={styles.detailsItem}>
<FontAwesomeIcon icon={faCalendarPlus} />{' '}
{createdAt ? formatTimestamp(createdAt) : <>&mdash;</>}
</span>
</Tooltip>
<Tooltip
title={'Completion date'}
placement="top"
arrow
disableInteractive
>
<span className={styles.detailsItem}>
<FontAwesomeIcon icon={faCalendarCheck} />{' '}
{completedAt ? formatTimestamp(completedAt) : <>&mdash;</>}
</span>
</Tooltip>
{/* User signed date */}
{userCanSign ? (
<Tooltip
title={'Your signature date'}
placement="top"
arrow
disableInteractive
>
<span className={styles.detailsItem}>
<FontAwesomeIcon icon={faCalendar} />{' '}
{hexToNpub(usersPubkey) in parsedSignatureEvents ? (
parsedSignatureEvents[hexToNpub(usersPubkey)].created_at ? (
formatTimestamp(
fromUnixTimestamp(
parsedSignatureEvents[hexToNpub(usersPubkey)].created_at
)
)
) : (
<>&mdash;</>
)
) : (
<>&mdash;</>
)}
</span>
</Tooltip>
) : null}
<span className={styles.detailsItem}>
<FontAwesomeIcon icon={faEye} /> {signedStatus}
</span>
{ext.length > 0 ? (
<span className={styles.detailsItem}>
{ext.length > 1 ? (
<>
<FontAwesomeIcon icon={faFile} /> Multiple File Types
</>
) : (
getExtensionIconLabel(ext[0])
)}
</span>
) : (
<>
<FontAwesomeIcon icon={faFileCircleExclamation} /> &mdash;
</>
)}
</div>
</div>
) : undefined
}

View File

@ -0,0 +1,224 @@
import { Divider, Tooltip } from '@mui/material'
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
import {
extractFileExtensions,
formatTimestamp,
fromUnixTimestamp,
hexToNpub,
npubToHex,
shorten,
SignStatus
} from '../../utils'
import { UserAvatar } from '../UserAvatar'
import { FlatMeta } from '../../hooks/useSigitMeta'
import { UserAvatarGroup } from '../UserAvatarGroup'
import styles from './style.module.scss'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faCalendar,
faCalendarCheck,
faCalendarPlus,
faEye,
faFile,
faFileCircleExclamation
} from '@fortawesome/free-solid-svg-icons'
import { getExtensionIconLabel } from '../getExtensionIconLabel'
import { useSelector } from 'react-redux'
import { State } from '../../store/rootReducer'
import { TooltipChild } from '../TooltipChild'
import { DisplaySigner } from '../DisplaySigner'
type UsersDetailsProps = Pick<
FlatMeta,
| 'submittedBy'
| 'signers'
| 'viewers'
| 'fileHashes'
| 'parsedSignatureEvents'
| 'createdAt'
| 'signedStatus'
| 'completedAt'
| 'signersStatus'
>
export const UsersDetails = ({
submittedBy,
signers,
viewers,
fileHashes,
parsedSignatureEvents,
createdAt,
signedStatus,
completedAt,
signersStatus
}: UsersDetailsProps) => {
const { usersPubkey } = useSelector((state: State) => state.auth)
const profiles = useSigitProfiles([
...(submittedBy ? [submittedBy] : []),
...signers,
...viewers
])
const userCanSign =
typeof usersPubkey !== 'undefined' &&
signers.includes(hexToNpub(usersPubkey))
const ext = extractFileExtensions(Object.keys(fileHashes))
return submittedBy ? (
<div className={styles.container}>
<div className={styles.section}>
<p>Signers</p>
<div className={styles.users}>
{submittedBy &&
(function () {
const profile = profiles[submittedBy]
return (
<Tooltip
title={
profile?.display_name ||
profile?.name ||
shorten(hexToNpub(submittedBy))
}
placement="top"
arrow
disableInteractive
>
<TooltipChild>
<UserAvatar pubkey={submittedBy} image={profile?.picture} />
</TooltipChild>
</Tooltip>
)
})()}
{submittedBy && signers.length ? (
<Divider orientation="vertical" flexItem />
) : null}
<UserAvatarGroup max={20}>
{signers.map((signer) => {
const pubkey = npubToHex(signer)!
const profile = profiles[pubkey]
return (
<Tooltip
key={signer}
title={
profile?.display_name || profile?.name || shorten(pubkey)
}
placement="top"
arrow
disableInteractive
>
<TooltipChild>
<DisplaySigner
status={signersStatus[pubkey as `npub1${string}`]}
profile={profile}
pubkey={pubkey}
/>
</TooltipChild>
</Tooltip>
)
})}
{viewers.map((signer) => {
const pubkey = npubToHex(signer)!
const profile = profiles[pubkey]
return (
<Tooltip
key={signer}
title={
profile?.display_name || profile?.name || shorten(pubkey)
}
placement="top"
arrow
disableInteractive
>
<TooltipChild>
<DisplaySigner
status={SignStatus.Viewer}
profile={profile}
pubkey={pubkey}
/>
</TooltipChild>
</Tooltip>
)
})}
</UserAvatarGroup>
</div>
</div>
<div className={styles.section}>
<p>Details</p>
<Tooltip
title={'Publication date'}
placement="top"
arrow
disableInteractive
>
<span className={styles.detailsItem}>
<FontAwesomeIcon icon={faCalendarPlus} />{' '}
{createdAt ? formatTimestamp(createdAt) : <>&mdash;</>}
</span>
</Tooltip>
<Tooltip
title={'Completion date'}
placement="top"
arrow
disableInteractive
>
<span className={styles.detailsItem}>
<FontAwesomeIcon icon={faCalendarCheck} />{' '}
{completedAt ? formatTimestamp(completedAt) : <>&mdash;</>}
</span>
</Tooltip>
{/* User signed date */}
{userCanSign ? (
<Tooltip
title={'Your signature date'}
placement="top"
arrow
disableInteractive
>
<span className={styles.detailsItem}>
<FontAwesomeIcon icon={faCalendar} />{' '}
{hexToNpub(usersPubkey) in parsedSignatureEvents ? (
parsedSignatureEvents[hexToNpub(usersPubkey)].created_at ? (
formatTimestamp(
fromUnixTimestamp(
parsedSignatureEvents[hexToNpub(usersPubkey)].created_at
)
)
) : (
<>&mdash;</>
)
) : (
<>&mdash;</>
)}
</span>
</Tooltip>
) : null}
<span className={styles.detailsItem}>
<FontAwesomeIcon icon={faEye} /> {signedStatus}
</span>
{ext.length > 0 ? (
<span className={styles.detailsItem}>
{ext.length > 1 ? (
<>
<FontAwesomeIcon icon={faFile} /> Multiple File Types
</>
) : (
getExtensionIconLabel(ext[0])
)}
</span>
) : (
<>
<FontAwesomeIcon icon={faFileCircleExclamation} /> &mdash;
</>
)}
</div>
</div>
) : undefined
}

View File

@ -17,6 +17,11 @@
grid-gap: 10px;
}
.users {
display: flex;
grid-gap: 10px;
}
.detailsItem {
transition: ease 0.2s;
color: rgba(0, 0, 0, 0.5);

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { CreateSignatureEventContent, Meta } from '../types'
import { CreateSignatureEventContent, Meta, SignedEventContent } from '../types'
import { Mark } from '../types/mark'
import {
fromUnixTimestamp,
@ -118,7 +118,6 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
if (meta.keys) {
const { sender, keys } = meta.keys
// Retrieve the user's public key from the state
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
const usersNpub = hexToNpub(usersPubkey)
@ -141,8 +140,28 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
}
}
// Temp. map to hold events
// Temp. map to hold events and signers
const parsedSignatureEventsMap = new Map<`npub1${string}`, Event>()
const signerStatusMap = new Map<`npub1${string}`, SignStatus>()
const getPrevSignerSig = (npub: `npub1${string}`) => {
if (signers[0] === npub) {
return sig
}
// find the index of signer
const currentSignerIndex = signers.findIndex(
(signer) => signer === npub
)
// return if could not found user in signer's list
if (currentSignerIndex === -1) return
// find prev signer
const prevSigner = signers[currentSignerIndex - 1]
// get the signature of prev signer
return parsedSignatureEventsMap.get(prevSigner)?.sig
}
for (const npub in meta.docSignatures) {
try {
// Parse each signature event
@ -150,34 +169,49 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
meta.docSignatures[npub as `npub1${string}`]
)
const isValidSignature = verifyEvent(event)
// Save events to a map, to save all at once outside loop
// We need the object to find completedAt
// Avoided using parsedSignatureEvents due to useEffect deps
parsedSignatureEventsMap.set(npub as `npub1${string}`, event)
setSignersStatus((prev) => {
return {
...prev,
[npub]: isValidSignature
? SignStatus.Signed
: SignStatus.Invalid
}
})
} catch (error) {
setSignersStatus((prev) => {
return {
...prev,
[npub]: SignStatus.Invalid
}
})
signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid)
}
}
setParsedSignatureEvents(
Object.fromEntries(parsedSignatureEventsMap.entries())
)
parsedSignatureEventsMap.forEach((event, npub) => {
const isValidSignature = verifyEvent(event)
if (isValidSignature) {
// get the signature of prev signer from the content of current signers signedEvent
const prevSignersSig = getPrevSignerSig(npub)
try {
const obj: SignedEventContent = JSON.parse(event.content)
if (
obj.prevSig &&
prevSignersSig &&
obj.prevSig === prevSignersSig
) {
signerStatusMap.set(npub as `npub1${string}`, SignStatus.Signed)
}
} catch (error) {
signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid)
}
}
})
const nextSigner = signers.find((s) => !parsedSignatureEventsMap.has(s))
if (nextSigner) {
signerStatusMap.set(nextSigner, SignStatus.Awaiting)
}
signers
.filter((s) => !(s in meta.docSignatures))
.forEach((s) =>
signerStatusMap.set(s as `npub1${string}`, SignStatus.Pending)
)
setSignersStatus(Object.fromEntries(signerStatusMap))
setParsedSignatureEvents(Object.fromEntries(parsedSignatureEventsMap))
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
const isCompletelySigned = signers.every((signer) =>
@ -187,7 +221,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial
)
// Check if all signers signed and are valid
// Check if all signers signed
if (isCompletelySigned) {
setCompletedAt(
fromUnixTimestamp(

View File

@ -15,7 +15,8 @@ import {
unixNow,
parseJson,
readContentOfZipEntry,
signEventForMetaFile
signEventForMetaFile,
shorten
} from '../../utils'
import styles from './style.module.scss'
import { Cancel, CheckCircle } from '@mui/icons-material'
@ -34,8 +35,11 @@ import { getLastSignersSig } from '../../utils/sign.ts'
import { saveAs } from 'file-saver'
import { Container } from '../../components/Container'
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
import { Files } from '../../layouts/Files.tsx'
import { FileUsers } from '../../components/FilesUsers.tsx/index.tsx'
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
import { UsersDetails } from '../../components/UsersDetails.tsx/index.tsx'
import { UserAvatar } from '../../components/UserAvatar/index.tsx'
import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx'
import { TooltipChild } from '../../components/TooltipChild.tsx'
export const VerifyPage = () => {
const theme = useTheme()
@ -50,8 +54,25 @@ export const VerifyPage = () => {
*/
const { uploadedZip, meta } = location.state || {}
const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } =
useSigitMeta(meta)
const {
submittedBy,
zipUrl,
encryptionKey,
signers,
viewers,
fileHashes,
parsedSignatureEvents,
createdAt,
signedStatus,
completedAt,
signersStatus
} = useSigitMeta(meta)
const profiles = useSigitProfiles([
...(submittedBy ? [submittedBy] : []),
...signers,
...viewers
])
const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
@ -339,7 +360,24 @@ export const VerifyPage = () => {
const exportSignatureEvent = JSON.parse(exportSignatureString) as Event
if (verifyEvent(exportSignatureEvent)) {
// return displayUser(exportSignatureEvent.pubkey)
const exportedBy = exportSignatureEvent.pubkey
const profile = profiles[exportedBy]
return (
<Tooltip
title={
profile?.display_name ||
profile?.name ||
shorten(hexToNpub(exportedBy))
}
placement="top"
arrow
disableInteractive
>
<TooltipChild>
<UserAvatar pubkey={exportedBy} image={profile?.picture} />
</TooltipChild>
</Tooltip>
)
} else {
toast.error(`Invalid export signature!`)
return (
@ -386,7 +424,7 @@ export const VerifyPage = () => {
)}
{meta && (
<Files
<StickySideColumns
left={
<>
<Box className={styles.filesWrapper}>
@ -432,8 +470,19 @@ export const VerifyPage = () => {
</Box>
</>
}
right={<FileUsers meta={meta} />}
content={<div style={{ height: '300vh' }}></div>}
right={
<UsersDetails
submittedBy={submittedBy}
signers={signers}
viewers={viewers}
fileHashes={fileHashes}
parsedSignatureEvents={parsedSignatureEvents}
createdAt={createdAt}
signedStatus={signedStatus}
completedAt={completedAt}
signersStatus={signersStatus}
/>
}
/>
)}
</Container>

View File

@ -5,8 +5,10 @@ import { toast } from 'react-toastify'
export enum SignStatus {
Signed = 'Signed',
Awaiting = 'Awaiting',
Pending = 'Pending',
Invalid = 'Invalid Sign'
Invalid = 'Invalid',
Viewer = 'Viewer'
}
export enum SigitStatus {