diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index dfbcbba..0d7407f 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -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) => { > diff --git a/src/components/DisplaySigner/index.tsx b/src/components/DisplaySigner/index.tsx index dc4b9ce..4f3824f 100644 --- a/src/components/DisplaySigner/index.tsx +++ b/src/components/DisplaySigner/index.tsx @@ -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() + const getStatusIcon = (status: SignStatus) => { + switch (status) { + case SignStatus.Signed: + return + case SignStatus.Awaiting: + return ( + + + + ) + case SignStatus.Pending: + return + case SignStatus.Invalid: + return + case SignStatus.Viewer: + return - useEffect(() => { - if (!meta) return - - const updateSignStatus = async () => { - const npub = hexToNpub(pubkey) - if (npub in meta.docSignatures) { - parseJson(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 } - - updateSignStatus() - }, [meta, pubkey]) + } return ( - {signStatus === SignStatus.Signed && ( - - )} - {signStatus === SignStatus.Invalid && ( - - )} - - ) +
{getStatusIcon(status)}
} > diff --git a/src/components/FilesUsers.tsx/index.tsx b/src/components/FilesUsers.tsx/index.tsx deleted file mode 100644 index 59e3a09..0000000 --- a/src/components/FilesUsers.tsx/index.tsx +++ /dev/null @@ -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 ( - <> - - - {verifySignature && ( - <> - {isValidSignature && ( - - - - )} - - {!isValidSignature && ( - - - - )} - - )} - - ) - } - - return submittedBy ? ( -
-
-

Signers

- {displayUser(submittedBy)} - {submittedBy && signers.length ? ( - - ) : null} - - {signers.length > 0 && - signers.map((signer) => ( - {displayUser(npubToHex(signer)!, true)} - ))} - {viewers.length > 0 && - viewers.map((viewer) => ( - {displayUser(npubToHex(viewer)!)} - ))} - -
-
-

Details

- - - - {' '} - {createdAt ? formatTimestamp(createdAt) : <>—} - - - - - - {' '} - {completedAt ? formatTimestamp(completedAt) : <>—} - - - - {/* User signed date */} - {userCanSign ? ( - - - {' '} - {hexToNpub(usersPubkey) in parsedSignatureEvents ? ( - parsedSignatureEvents[hexToNpub(usersPubkey)].created_at ? ( - formatTimestamp( - fromUnixTimestamp( - parsedSignatureEvents[hexToNpub(usersPubkey)].created_at - ) - ) - ) : ( - <>— - ) - ) : ( - <>— - )} - - - ) : null} - - {signedStatus} - - {ext.length > 0 ? ( - - {ext.length > 1 ? ( - <> - Multiple File Types - - ) : ( - getExtensionIconLabel(ext[0]) - )} - - ) : ( - <> - — - - )} -
-
- ) : undefined -} diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx new file mode 100644 index 0000000..8b9217b --- /dev/null +++ b/src/components/UsersDetails.tsx/index.tsx @@ -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 ? ( +
+
+

Signers

+
+ {submittedBy && + (function () { + const profile = profiles[submittedBy] + return ( + + + + + + ) + })()} + + {submittedBy && signers.length ? ( + + ) : null} + + + {signers.map((signer) => { + const pubkey = npubToHex(signer)! + const profile = profiles[pubkey] + + return ( + + + + + + ) + })} + {viewers.map((signer) => { + const pubkey = npubToHex(signer)! + const profile = profiles[pubkey] + + return ( + + + + + + ) + })} + +
+
+
+

Details

+ + + + {' '} + {createdAt ? formatTimestamp(createdAt) : <>—} + + + + + + {' '} + {completedAt ? formatTimestamp(completedAt) : <>—} + + + + {/* User signed date */} + {userCanSign ? ( + + + {' '} + {hexToNpub(usersPubkey) in parsedSignatureEvents ? ( + parsedSignatureEvents[hexToNpub(usersPubkey)].created_at ? ( + formatTimestamp( + fromUnixTimestamp( + parsedSignatureEvents[hexToNpub(usersPubkey)].created_at + ) + ) + ) : ( + <>— + ) + ) : ( + <>— + )} + + + ) : null} + + {signedStatus} + + {ext.length > 0 ? ( + + {ext.length > 1 ? ( + <> + Multiple File Types + + ) : ( + getExtensionIconLabel(ext[0]) + )} + + ) : ( + <> + — + + )} +
+
+ ) : undefined +} diff --git a/src/components/FilesUsers.tsx/style.module.scss b/src/components/UsersDetails.tsx/style.module.scss similarity index 93% rename from src/components/FilesUsers.tsx/style.module.scss rename to src/components/UsersDetails.tsx/style.module.scss index b6e0313..9d906c1 100644 --- a/src/components/FilesUsers.tsx/style.module.scss +++ b/src/components/UsersDetails.tsx/style.module.scss @@ -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); diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index 78516d5..ca8ea6d 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -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( diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index d6a297d..bd35f14 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -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 ( + + + + + + ) } else { toast.error(`Invalid export signature!`) return ( @@ -386,7 +424,7 @@ export const VerifyPage = () => { )} {meta && ( - @@ -432,8 +470,19 @@ export const VerifyPage = () => { } - right={} - content={
} + right={ + + } /> )} diff --git a/src/utils/meta.ts b/src/utils/meta.ts index dd29b60..4915f19 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -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 {