From d9779c10bde5a510f2ac8daf660857299dc585e9 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 13 Aug 2024 13:32:32 +0200 Subject: [PATCH 01/10] refactor: flat meta and add useSigitProfile --- src/components/DisplaySigit/index.tsx | 65 ++-------------- src/hooks/useSigitMeta.tsx | 70 ++++++++++++++--- src/hooks/useSigitProfiles.tsx | 70 +++++++++++++++++ src/pages/verify/index.tsx | 107 +++++--------------------- src/utils/meta.ts | 4 +- 5 files changed, 155 insertions(+), 161 deletions(-) create mode 100644 src/hooks/useSigitProfiles.tsx diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index 0fe5c2f..ce82f47 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -1,9 +1,6 @@ -import { useEffect, useState } from 'react' -import { Meta, ProfileMetadata } from '../../types' +import { Meta } from '../../types' import { SigitCardDisplayInfo, SigitStatus } from '../../utils' -import { Event, kinds } from 'nostr-tools' import { Link } from 'react-router-dom' -import { MetadataController } from '../../controllers' import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils' import { appPublicRoutes, appPrivateRoutes } from '../../routes' import { Button, Divider, Tooltip } from '@mui/material' @@ -22,6 +19,7 @@ import { UserAvatarGroup } from '../UserAvatarGroup' import styles from './style.module.scss' import { TooltipChild } from '../TooltipChild' import { getExtensionIconLabel } from '../getExtensionIconLabel' +import { useSigitProfiles } from '../../hooks/useSigitProfiles' type SigitProps = { meta: Meta @@ -38,61 +36,10 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { fileExtensions } = parsedMeta - const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>( - {} - ) - - useEffect(() => { - const hexKeys = new Set([ - ...signers.map((signer) => npubToHex(signer)!) - ]) - - if (submittedBy) { - hexKeys.add(npubToHex(submittedBy)!) - } - - 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)) { - metadataController.on(key, handleEventListener(key)) - - metadataController - .findMetadata(key) - .then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(key)(metadataEvent) - }) - .catch((err) => { - console.error(`error occurred in finding metadata for: ${key}`, err) - }) - } - }) - - return () => { - hexKeys.forEach((key) => { - metadataController.off(key, handleEventListener(key)) - }) - } - }, [submittedBy, signers, profiles]) + const profiles = useSigitProfiles([ + ...(submittedBy ? [submittedBy] : []), + ...signers + ]) return (
diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index aebd791..5460983 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -3,6 +3,7 @@ import { CreateSignatureEventContent, Meta } from '../types' import { Mark } from '../types/mark' import { fromUnixTimestamp, + hexToNpub, parseCreateSignatureEvent, parseCreateSignatureEventContent, SigitMetaParseError, @@ -12,11 +13,30 @@ import { import { toast } from 'react-toastify' import { verifyEvent } from 'nostr-tools' import { Event } from 'nostr-tools' +import store from '../store/store' +import { AuthState } from '../store/auth/types' +import { NostrController } from '../controllers' + +/** + * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`, + * and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions) + */ +interface FlatMeta + extends Meta, + CreateSignatureEventContent, + Partial> { + // Remove pubkey and use submittedBy as `npub1${string}` + submittedBy?: `npub1${string}` + + // Remove created_at and replace with createdAt + createdAt?: number -interface FlatMeta extends Meta, CreateSignatureEventContent, Partial { // Validated create signature event isValid: boolean + // Decryption + encryptionKey: string | null + // Calculated status fields signedStatus: SigitStatus signersStatus: { @@ -33,8 +53,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { const [isValid, setIsValid] = useState(false) const [kind, setKind] = useState() const [tags, setTags] = useState() - const [created_at, setCreatedAt] = useState() - const [pubkey, setPubkey] = useState() // submittedBy, pubkey from nostr event + const [createdAt, setCreatedAt] = useState() + const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event const [id, setId] = useState() const [sig, setSig] = useState() @@ -54,6 +74,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { [signer: `npub1${string}`]: SignStatus }>({}) + const [encryptionKey, setEncryptionKey] = useState(null) + useEffect(() => { if (!meta) return ;(async function () { @@ -70,7 +92,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { setTags(tags) // created_at in nostr events are stored in seconds setCreatedAt(fromUnixTimestamp(created_at)) - setPubkey(pubkey) + setSubmittedBy(pubkey as `npub1${string}`) setId(id) setSig(sig) @@ -84,6 +106,31 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { setMarkConfig(markConfig) setZipUrl(zipUrl) + 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) + + // Check if the user's public key is in the keys object + if (usersNpub in keys) { + // Instantiate the NostrController to decrypt the encryption key + const nostrController = NostrController.getInstance() + const decrypted = await nostrController + .nip04Decrypt(sender, keys[usersNpub]) + .catch((err) => { + console.log( + 'An error occurred in decrypting encryption key', + err + ) + return null + }) + + setEncryptionKey(decrypted) + } + } + // Parse each signature event and set signer status for (const npub in meta.docSignatures) { try { @@ -125,15 +172,15 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { }, [meta]) return { - modifiedAt: meta.modifiedAt, - createSignature: meta.createSignature, - docSignatures: meta.docSignatures, - keys: meta.keys, + modifiedAt: meta?.modifiedAt, + createSignature: meta?.createSignature, + docSignatures: meta?.docSignatures, + keys: meta?.keys, isValid, kind, tags, - created_at, - pubkey, + createdAt, + submittedBy, id, sig, signers, @@ -143,6 +190,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { title, zipUrl, signedStatus, - signersStatus + signersStatus, + encryptionKey } } diff --git a/src/hooks/useSigitProfiles.tsx b/src/hooks/useSigitProfiles.tsx new file mode 100644 index 0000000..8178dd7 --- /dev/null +++ b/src/hooks/useSigitProfiles.tsx @@ -0,0 +1,70 @@ +import { useEffect, useState } from 'react' +import { ProfileMetadata } from '../types' +import { MetadataController } from '../controllers' +import { npubToHex } from '../utils' +import { Event, kinds } from 'nostr-tools' + +/** + * Extracts profiles from metadata events + * @param pubkeys Array of npubs to check + * @returns ProfileMetadata + */ +export const useSigitProfiles = ( + pubkeys: `npub1${string}`[] +): { [key: string]: ProfileMetadata } => { + const [profileMetadata, setProfileMetadata] = useState<{ + [key: string]: ProfileMetadata + }>({}) + + useEffect(() => { + if (pubkeys.length) { + const metadataController = new MetadataController() + + // Remove duplicate keys + const users = new Set([...pubkeys]) + + const handleMetadataEvent = (key: string) => (event: Event) => { + const metadataContent = + metadataController.extractProfileMetadataContent(event) + + if (metadataContent) { + setProfileMetadata((prev) => ({ + ...prev, + [key]: metadataContent + })) + } + } + + users.forEach((user) => { + const hexKey = npubToHex(user) + if (hexKey && !(hexKey in profileMetadata)) { + metadataController.on(hexKey, (kind: number, event: Event) => { + if (kind === kinds.Metadata) { + handleMetadataEvent(hexKey)(event) + } + }) + + metadataController + .findMetadata(hexKey) + .then((metadataEvent) => { + if (metadataEvent) handleMetadataEvent(hexKey)(metadataEvent) + }) + .catch((err) => { + console.error( + `error occurred in finding metadata for: ${user}`, + err + ) + }) + } + }) + + return () => { + users.forEach((key) => { + metadataController.off(key, handleMetadataEvent(key)) + }) + } + } + }, [pubkeys, profileMetadata]) + + return profileMetadata +} diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 1f6bee1..f77bd48 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -10,22 +10,20 @@ import { } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' -import { Event, kinds, verifyEvent } from 'nostr-tools' +import { Event, verifyEvent } from 'nostr-tools' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { UserAvatar } from '../../components/UserAvatar' -import { MetadataController, NostrController } from '../../controllers' +import { NostrController } from '../../controllers' import { CreateSignatureEventContent, Meta, - ProfileMetadata, SignedEventContent } from '../../types' import { decryptArrayBuffer, extractMarksFromSignedMeta, - extractZipUrlAndEncryptionKey, getHash, hexToNpub, unixNow, @@ -51,6 +49,8 @@ import { useSelector } from 'react-redux' import { getLastSignersSig } from '../../utils/sign.ts' import { saveAs } from 'file-saver' import { Container } from '../../components/Container' +import { useSigitMeta } from '../../hooks/useSigitMeta.tsx' +import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx' export const VerifyPage = () => { const theme = useTheme() @@ -63,52 +63,35 @@ export const VerifyPage = () => { * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json * meta will be received in navigation from create & home page in online mode */ - const { uploadedZip, meta: metaInNavState } = location.state || {} + const { uploadedZip, meta } = location.state || {} + + const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } = + useSigitMeta(meta) + const profiles = useSigitProfiles([ + ...(submittedBy ? [submittedBy] : []), + ...signers, + ...viewers + ]) const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [selectedFile, setSelectedFile] = useState(null) - const [meta, setMeta] = useState(null) - const [submittedBy, setSubmittedBy] = useState() - - const [signers, setSigners] = useState<`npub1${string}`[]>([]) - const [viewers, setViewers] = useState<`npub1${string}`[]>([]) - const [creatorFileHashes, setCreatorFileHashes] = useState<{ - [key: string]: string - }>({}) const [currentFileHashes, setCurrentFileHashes] = useState<{ [key: string]: string | null - }>({}) + }>(fileHashes) const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) - const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( - {} - ) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const nostrController = NostrController.getInstance() useEffect(() => { if (uploadedZip) { setSelectedFile(uploadedZip) - } else if (metaInNavState) { + } else if (meta && encryptionKey) { const processSigit = async () => { setIsLoading(true) - setLoadingSpinnerDesc('Extracting zipUrl and encryption key from meta') - - const res = await extractZipUrlAndEncryptionKey(metaInNavState) - if (!res) { - setIsLoading(false) - return - } - - const { - zipUrl, - encryptionKey, - createSignatureEvent, - createSignatureContent - } = res setLoadingSpinnerDesc('Fetching file from file server') axios @@ -175,12 +158,6 @@ export const VerifyPage = () => { setCurrentFileHashes(fileHashes) setFiles(files) - setSigners(createSignatureContent.signers) - setViewers(createSignatureContent.viewers) - setCreatorFileHashes(createSignatureContent.fileHashes) - setSubmittedBy(createSignatureEvent.pubkey) - - setMeta(metaInNavState) setIsLoading(false) } }) @@ -197,49 +174,7 @@ export const VerifyPage = () => { processSigit() } - }, [uploadedZip, metaInNavState]) - - useEffect(() => { - if (submittedBy) { - const metadataController = new MetadataController() - - const users = [submittedBy, ...signers, ...viewers] - - users.forEach((user) => { - const pubkey = npubToHex(user)! - - if (!(pubkey in metadata)) { - const handleMetadataEvent = (event: Event) => { - const metadataContent = - metadataController.extractProfileMetadataContent(event) - if (metadataContent) - setMetadata((prev) => ({ - ...prev, - [pubkey]: metadataContent - })) - } - - metadataController.on(pubkey, (kind: number, event: Event) => { - if (kind === kinds.Metadata) { - handleMetadataEvent(event) - } - }) - - metadataController - .findMetadata(pubkey) - .then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(metadataEvent) - }) - .catch((err) => { - console.error( - `error occurred in finding metadata for: ${user}`, - err - ) - }) - } - }) - } - }, [submittedBy, signers, viewers, metadata]) + }, [encryptionKey, meta, uploadedZip, zipUrl]) const handleVerify = async () => { if (!selectedFile) return @@ -345,12 +280,6 @@ export const VerifyPage = () => { if (!createSignatureContent) return - setSigners(createSignatureContent.signers) - setViewers(createSignatureContent.viewers) - setCreatorFileHashes(createSignatureContent.fileHashes) - setSubmittedBy(createSignatureEvent.pubkey) - - setMeta(parsedMetaJson) setIsLoading(false) } @@ -451,7 +380,7 @@ export const VerifyPage = () => { } const displayUser = (pubkey: string, verifySignature = false) => { - const profile = metadata[pubkey] + const profile = profiles[pubkey] let isValidSignature = false @@ -682,7 +611,7 @@ export const VerifyPage = () => { {Object.entries(currentFileHashes).map( ([filename, hash], index) => { - const isValidHash = creatorFileHashes[filename] === hash + const isValidHash = fileHashes[filename] === hash return ( diff --git a/src/utils/meta.ts b/src/utils/meta.ts index b3c0c28..74d38b7 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -69,7 +69,7 @@ export enum SigitMetaParseErrorType { export interface SigitCardDisplayInfo { createdAt?: number title?: string - submittedBy?: string + submittedBy?: `npub1${string}` signers: `npub1${string}`[] fileExtensions: string[] signedStatus: SigitStatus @@ -161,7 +161,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { ) sigitInfo.title = createSignatureContent.title - sigitInfo.submittedBy = createSignatureEvent.pubkey + sigitInfo.submittedBy = createSignatureEvent.pubkey as `npub1${string}` sigitInfo.signers = createSignatureContent.signers sigitInfo.fileExtensions = extensions From bc23361fb037779de5d7c6fe89cdbefd2ce0479d Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 13 Aug 2024 17:26:21 +0200 Subject: [PATCH 02/10] refactor: move styles from page to Avatar Group component --- src/components/DisplaySigit/index.tsx | 2 +- src/components/DisplaySigit/style.module.scss | 20 ------------------- src/components/UserAvatarGroup/index.tsx | 2 +- .../UserAvatarGroup/style.module.scss | 20 +++++++++++++++++++ 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index ce82f47..dfbcbba 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -77,7 +77,7 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { {submittedBy && signers.length ? ( ) : null} - + {signers.map((signer) => { const pubkey = npubToHex(signer)! const profile = profiles[pubkey] diff --git a/src/components/DisplaySigit/style.module.scss b/src/components/DisplaySigit/style.module.scss index 7544fc4..4bb2f15 100644 --- a/src/components/DisplaySigit/style.module.scss +++ b/src/components/DisplaySigit/style.module.scss @@ -93,26 +93,6 @@ grid-gap: 10px; } -.signers { - padding: 0 0 0 10px; - - > * { - transition: margin ease 0.2s; - margin: 0 0 0 -10px; - position: relative; - z-index: 1; - &:first-child { - margin-left: -10px !important; - } - } - - > *:hover, - > *:focus-within { - margin: 0 15px 0 5px; - z-index: 2; - } -} - .details { color: rgba(0, 0, 0, 0.3); font-size: 14px; diff --git a/src/components/UserAvatarGroup/index.tsx b/src/components/UserAvatarGroup/index.tsx index 13f8b25..f8e231f 100644 --- a/src/components/UserAvatarGroup/index.tsx +++ b/src/components/UserAvatarGroup/index.tsx @@ -28,7 +28,7 @@ export const UserAvatarGroup = ({ const childrenArray = Children.toArray(children) return ( -
+
{surplus > 1 ? childrenArray.slice(0, surplus * -1).map((c) => c) : children} diff --git a/src/components/UserAvatarGroup/style.module.scss b/src/components/UserAvatarGroup/style.module.scss index 9604202..c9ee551 100644 --- a/src/components/UserAvatarGroup/style.module.scss +++ b/src/components/UserAvatarGroup/style.module.scss @@ -1,5 +1,25 @@ @import '../../styles/colors.scss'; +.container { + padding: 0 0 0 10px; + + > * { + transition: margin ease 0.2s; + margin: 0 0 0 -10px; + position: relative; + z-index: 1; + &:first-child { + margin-left: -10px !important; + } + } + + > *:hover, + > *:focus-within { + margin: 0 15px 0 5px; + z-index: 2; + } +} + .icon { width: 40px; height: 40px; From e16b8cfe3fe297983a3fd122ac25b89ca1568835 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 13 Aug 2024 17:27:08 +0200 Subject: [PATCH 03/10] feat: add sticky layout with slots --- src/layouts/Files.module.scss | 24 ++++++++++++++++++++++++ src/layouts/Files.tsx | 29 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/layouts/Files.module.scss create mode 100644 src/layouts/Files.tsx diff --git a/src/layouts/Files.module.scss b/src/layouts/Files.module.scss new file mode 100644 index 0000000..bda18dc --- /dev/null +++ b/src/layouts/Files.module.scss @@ -0,0 +1,24 @@ +@import '../styles/colors.scss'; +@import '../styles/sizes.scss'; + +.container { + display: grid; + grid-template-columns: 0.75fr 1.5fr 0.75fr; + grid-gap: 30px; + flex-grow: 1; +} + +.sidesWrap { + position: relative; +} + +.sides { + position: sticky; + top: $header-height + $body-vertical-padding; +} + +.files { + display: flex; + flex-direction: column; + grid-gap: 15px; +} diff --git a/src/layouts/Files.tsx b/src/layouts/Files.tsx new file mode 100644 index 0000000..a494293 --- /dev/null +++ b/src/layouts/Files.tsx @@ -0,0 +1,29 @@ +import { PropsWithChildren, ReactNode } from 'react' + +import styles from './Files.module.scss' + +interface FilesProps { + left: ReactNode + right: ReactNode + content: ReactNode +} + +export const Files = ({ + left, + right, + content, + children +}: PropsWithChildren) => { + return ( +
+
+
{left}
+
+
{content}
+
+
{right}
+
+ {children} +
+ ) +} From b3155cce0d9cc6be4fc20c9130988c76ea417784 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 13 Aug 2024 17:28:14 +0200 Subject: [PATCH 04/10] refactor: expand useSigitMeta and update verify w details section --- src/components/FilesUsers.tsx/index.tsx | 246 +++++++++++++++++ .../FilesUsers.tsx/style.module.scss | 41 +++ src/hooks/useSigitMeta.tsx | 54 +++- src/pages/verify/index.tsx | 249 ++---------------- src/utils/meta.ts | 47 ++-- 5 files changed, 376 insertions(+), 261 deletions(-) create mode 100644 src/components/FilesUsers.tsx/index.tsx create mode 100644 src/components/FilesUsers.tsx/style.module.scss diff --git a/src/components/FilesUsers.tsx/index.tsx b/src/components/FilesUsers.tsx/index.tsx new file mode 100644 index 0000000..59e3a09 --- /dev/null +++ b/src/components/FilesUsers.tsx/index.tsx @@ -0,0 +1,246 @@ +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/FilesUsers.tsx/style.module.scss b/src/components/FilesUsers.tsx/style.module.scss new file mode 100644 index 0000000..b6e0313 --- /dev/null +++ b/src/components/FilesUsers.tsx/style.module.scss @@ -0,0 +1,41 @@ +@import '../../styles/colors.scss'; + +.container { + border-radius: 4px; + background: $overlay-background-color; + padding: 15px; + display: flex; + flex-direction: column; + grid-gap: 25px; + + font-size: 14px; +} + +.section { + display: flex; + flex-direction: column; + grid-gap: 10px; +} + +.detailsItem { + transition: ease 0.2s; + color: rgba(0, 0, 0, 0.5); + font-size: 14px; + align-items: center; + border-radius: 4px; + padding: 5px; + + display: flex; + align-items: center; + justify-content: start; + + > :first-child { + padding: 5px; + margin-right: 10px; + } + + &:hover { + background: $primary-main; + color: white; + } +} diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index 5460983..78516d5 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -4,7 +4,7 @@ import { Mark } from '../types/mark' import { fromUnixTimestamp, hexToNpub, - parseCreateSignatureEvent, + parseNostrEvent, parseCreateSignatureEventContent, SigitMetaParseError, SigitStatus, @@ -21,7 +21,7 @@ import { NostrController } from '../controllers' * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`, * and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions) */ -interface FlatMeta +export interface FlatMeta extends Meta, CreateSignatureEventContent, Partial> { @@ -37,6 +37,12 @@ interface FlatMeta // Decryption encryptionKey: string | null + // Parsed Document Signatures + parsedSignatureEvents: { [signer: `npub1${string}`]: Event } + + // Calculated completion time + completedAt?: number + // Calculated status fields signedStatus: SigitStatus signersStatus: { @@ -67,6 +73,12 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { const [title, setTitle] = useState('') const [zipUrl, setZipUrl] = useState('') + const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{ + [signer: `npub1${string}`]: Event + }>({}) + + const [completedAt, setCompletedAt] = useState() + const [signedStatus, setSignedStatus] = useState( SigitStatus.Partial ) @@ -80,9 +92,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { if (!meta) return ;(async function () { try { - const createSignatureEvent = await parseCreateSignatureEvent( - meta.createSignature - ) + const createSignatureEvent = await parseNostrEvent(meta.createSignature) const { kind, tags, created_at, pubkey, id, sig, content } = createSignatureEvent @@ -131,13 +141,22 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { } } - // Parse each signature event and set signer status + // Temp. map to hold events + const parsedSignatureEventsMap = new Map<`npub1${string}`, Event>() for (const npub in meta.docSignatures) { try { - const event = await parseCreateSignatureEvent( + // Parse each signature event + const event = await parseNostrEvent( 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, @@ -155,6 +174,11 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { }) } } + + setParsedSignatureEvents( + Object.fromEntries(parsedSignatureEventsMap.entries()) + ) + const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] const isCompletelySigned = signers.every((signer) => signedBy.includes(signer) @@ -162,6 +186,20 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { setSignedStatus( isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial ) + + // Check if all signers signed and are valid + if (isCompletelySigned) { + setCompletedAt( + fromUnixTimestamp( + signedBy.reduce((p, c) => { + return Math.max( + p, + parsedSignatureEventsMap.get(c)?.created_at || 0 + ) + }, 0) + ) + ) + } } catch (error) { if (error instanceof SigitMetaParseError) { toast.error(error.message) @@ -189,6 +227,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { markConfig, title, zipUrl, + parsedSignatureEvents, + completedAt, signedStatus, signersStatus, encryptionKey diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index f77bd48..d6a297d 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -1,36 +1,20 @@ -import { - Box, - Button, - List, - ListItem, - ListSubheader, - Tooltip, - Typography, - useTheme -} from '@mui/material' +import { Box, Button, Tooltip, Typography, useTheme } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' import { Event, verifyEvent } from 'nostr-tools' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' -import { UserAvatar } from '../../components/UserAvatar' import { NostrController } from '../../controllers' -import { - CreateSignatureEventContent, - Meta, - SignedEventContent -} from '../../types' +import { CreateSignatureEventContent, Meta } from '../../types' import { decryptArrayBuffer, extractMarksFromSignedMeta, getHash, hexToNpub, unixNow, - npubToHex, parseJson, readContentOfZipEntry, - shorten, signEventForMetaFile } from '../../utils' import styles from './style.module.scss' @@ -50,7 +34,8 @@ import { getLastSignersSig } from '../../utils/sign.ts' import { saveAs } from 'file-saver' import { Container } from '../../components/Container' import { useSigitMeta } from '../../hooks/useSigitMeta.tsx' -import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx' +import { Files } from '../../layouts/Files.tsx' +import { FileUsers } from '../../components/FilesUsers.tsx/index.tsx' export const VerifyPage = () => { const theme = useTheme() @@ -67,11 +52,6 @@ export const VerifyPage = () => { const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } = useSigitMeta(meta) - const profiles = useSigitProfiles([ - ...(submittedBy ? [submittedBy] : []), - ...signers, - ...viewers - ]) const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -283,35 +263,6 @@ export const VerifyPage = () => { setIsLoading(false) } - const getPrevSignersSig = (npub: string) => { - if (!meta) return null - - // if user is first signer then use creator's signature - if (signers[0] === npub) { - try { - const createSignatureEvent: Event = JSON.parse(meta.createSignature) - return createSignatureEvent.sig - } catch (error) { - return null - } - } - - // 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: Event = JSON.parse(meta.docSignatures[prevSigner]) - return prevSignersEvent.sig - } catch (error) { - return null - } - } - const handleExport = async () => { if (Object.entries(files).length === 0 || !meta || !usersPubkey) return @@ -379,76 +330,6 @@ export const VerifyPage = () => { setIsLoading(false) } - const displayUser = (pubkey: string, verifySignature = false) => { - const profile = profiles[pubkey] - - let isValidSignature = false - - if (verifySignature) { - const npub = hexToNpub(pubkey) - const signedEventString = meta ? meta.docSignatures[npub] : null - 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 && ( - - - - )} - - )} - - ) - } - const displayExportedBy = () => { if (!meta || !meta.exportSignature) return null @@ -458,7 +339,7 @@ export const VerifyPage = () => { const exportSignatureEvent = JSON.parse(exportSignatureString) as Event if (verifyEvent(exportSignatureEvent)) { - return displayUser(exportSignatureEvent.pubkey) + // return displayUser(exportSignatureEvent.pubkey) } else { toast.error(`Invalid export signature!`) return ( @@ -505,109 +386,9 @@ export const VerifyPage = () => { )} {meta && ( - <> - - Meta Info - - } - > - {submittedBy && ( - - - Submitted By - - {displayUser(submittedBy)} - - )} - - - - Exported By - - {displayExportedBy()} - - - - - - {signers.length > 0 && ( - - - Signers - -
    - {signers.map((signer) => ( -
  • - {displayUser(npubToHex(signer)!, true)} -
  • - ))} -
-
- )} - - {viewers.length > 0 && ( - - - Viewers - -
    - {viewers.map((viewer) => ( -
  • - {displayUser(npubToHex(viewer)!)} -
  • - ))} -
-
- )} - - - - Files - + {Object.entries(currentFileHashes).map( ([filename, hash], index) => { @@ -643,9 +424,17 @@ export const VerifyPage = () => { } )} - -
- + {displayExportedBy()} + + + + + } + right={} + content={
} + /> )} diff --git a/src/utils/meta.ts b/src/utils/meta.ts index 74d38b7..dd29b60 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -62,7 +62,7 @@ function handleError(error: unknown): Error { // Reuse common error messages for meta parsing export enum SigitMetaParseErrorType { - 'PARSE_ERROR_SIGNATURE_EVENT' = 'error occurred in parsing the create signature event', + 'PARSE_ERROR_EVENT' = 'error occurred in parsing the create signature event', 'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content" } @@ -76,24 +76,19 @@ export interface SigitCardDisplayInfo { } /** - * Wrapper for createSignatureEvent parse that throws custom SigitMetaParseError with cause and context + * Wrapper for event parser that throws custom SigitMetaParseError with cause and context * @param raw Raw string for parsing * @returns parsed Event */ -export const parseCreateSignatureEvent = async ( - raw: string -): Promise => { +export const parseNostrEvent = async (raw: string): Promise => { try { - const createSignatureEvent = await parseJson(raw) - return createSignatureEvent + const event = await parseJson(raw) + return event } catch (error) { - throw new SigitMetaParseError( - SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT, - { - cause: handleError(error), - context: raw - } - ) + throw new SigitMetaParseError(SigitMetaParseErrorType.PARSE_ERROR_EVENT, { + cause: handleError(error), + context: raw + }) } } @@ -135,9 +130,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { } try { - const createSignatureEvent = await parseCreateSignatureEvent( - meta.createSignature - ) + const createSignatureEvent = await parseNostrEvent(meta.createSignature) // created_at in nostr events are stored in seconds sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at) @@ -147,13 +140,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { ) 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 extensions = extractFileExtensions(files) const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] const isCompletelySigned = createSignatureContent.signers.every((signer) => @@ -179,3 +166,15 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { } } } + +export const extractFileExtensions = (fileNames: string[]) => { + const extensions = fileNames.reduce((result: string[], file: string) => { + const extension = file.split('.').pop() + if (extension) { + result.push(extension) + } + return result + }, []) + + return extensions +} From 5ffedb68d67a1a60177861d0897341ab10741558 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 11:20:48 +0200 Subject: [PATCH 05/10] refactor: rename Files to StickySideColumns and update css --- ...odule.scss => StickySideColumns.module.scss} | 5 +++++ .../{Files.tsx => StickySideColumns.tsx} | 17 +++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) rename src/layouts/{Files.module.scss => StickySideColumns.module.scss} (84%) rename src/layouts/{Files.tsx => StickySideColumns.tsx} (58%) diff --git a/src/layouts/Files.module.scss b/src/layouts/StickySideColumns.module.scss similarity index 84% rename from src/layouts/Files.module.scss rename to src/layouts/StickySideColumns.module.scss index bda18dc..63c4314 100644 --- a/src/layouts/Files.module.scss +++ b/src/layouts/StickySideColumns.module.scss @@ -22,3 +22,8 @@ flex-direction: column; grid-gap: 15px; } +.content { + max-width: 550px; + width: 100%; + margin: 0 auto; +} diff --git a/src/layouts/Files.tsx b/src/layouts/StickySideColumns.tsx similarity index 58% rename from src/layouts/Files.tsx rename to src/layouts/StickySideColumns.tsx index a494293..1ada87f 100644 --- a/src/layouts/Files.tsx +++ b/src/layouts/StickySideColumns.tsx @@ -1,29 +1,26 @@ import { PropsWithChildren, ReactNode } from 'react' -import styles from './Files.module.scss' +import styles from './StickySideColumns.module.scss' -interface FilesProps { - left: ReactNode - right: ReactNode - content: ReactNode +interface StickySideColumnsProps { + left?: ReactNode + right?: ReactNode } -export const Files = ({ +export const StickySideColumns = ({ left, right, - content, children -}: PropsWithChildren) => { +}: PropsWithChildren) => { return (
{left}
-
{content}
+
{children}
{right}
- {children}
) } From 01ca81be2a431e8242cbae50f2e58115fc17a335 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 14:25:33 +0200 Subject: [PATCH 06/10] feat: add simple spinner wrapper --- src/components/Spinner/index.tsx | 6 ++++++ src/components/Spinner/style.module.scss | 12 ++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/components/Spinner/index.tsx create mode 100644 src/components/Spinner/style.module.scss diff --git a/src/components/Spinner/index.tsx b/src/components/Spinner/index.tsx new file mode 100644 index 0000000..cbc6b43 --- /dev/null +++ b/src/components/Spinner/index.tsx @@ -0,0 +1,6 @@ +import { PropsWithChildren } from 'react' +import styles from './style.module.scss' + +export const Spinner = ({ children }: PropsWithChildren) => ( +
{children}
+) diff --git a/src/components/Spinner/style.module.scss b/src/components/Spinner/style.module.scss new file mode 100644 index 0000000..08d8032 --- /dev/null +++ b/src/components/Spinner/style.module.scss @@ -0,0 +1,12 @@ +.spin { + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} From 4b04bdf39e5cb66c9c4ae45623d4f86432628a32 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 14:27:49 +0200 Subject: [PATCH 07/10] refactor: useSigitMeta, DisplaySigners and UserDetails section --- src/components/DisplaySigit/index.tsx | 5 +- src/components/DisplaySigner/index.tsx | 81 +++--- src/components/FilesUsers.tsx/index.tsx | 246 ------------------ src/components/UsersDetails.tsx/index.tsx | 224 ++++++++++++++++ .../style.module.scss | 5 + src/hooks/useSigitMeta.tsx | 82 ++++-- src/pages/verify/index.tsx | 67 ++++- src/utils/meta.ts | 4 +- 8 files changed, 384 insertions(+), 330 deletions(-) delete mode 100644 src/components/FilesUsers.tsx/index.tsx create mode 100644 src/components/UsersDetails.tsx/index.tsx rename src/components/{FilesUsers.tsx => UsersDetails.tsx}/style.module.scss (93%) 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 { From 3743a30ef62084c6c3a8cfdfcb63d1f08f0162ed Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 14:31:26 +0200 Subject: [PATCH 08/10] fix: use correct key for signer status, update signer badge icons --- src/components/DisplaySigner/index.tsx | 3 ++- src/components/UsersDetails.tsx/index.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/DisplaySigner/index.tsx b/src/components/DisplaySigner/index.tsx index 4f3824f..63aa154 100644 --- a/src/components/DisplaySigner/index.tsx +++ b/src/components/DisplaySigner/index.tsx @@ -5,6 +5,7 @@ import { UserAvatar } from '../UserAvatar' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCheck, + faEllipsis, faExclamation, faEye, faHourglass, @@ -35,7 +36,7 @@ export const DisplaySigner = ({ ) case SignStatus.Pending: - return + return case SignStatus.Invalid: return case SignStatus.Viewer: diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx index 8b9217b..ea58f68 100644 --- a/src/components/UsersDetails.tsx/index.tsx +++ b/src/components/UsersDetails.tsx/index.tsx @@ -112,7 +112,7 @@ export const UsersDetails = ({ > From d8adb2c74471bf55351b3649c699f8b6d4360a47 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 14:34:51 +0200 Subject: [PATCH 09/10] fix: next signer and spinner anim duration --- src/components/Spinner/style.module.scss | 2 +- src/hooks/useSigitMeta.tsx | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/Spinner/style.module.scss b/src/components/Spinner/style.module.scss index 08d8032..60158f4 100644 --- a/src/components/Spinner/style.module.scss +++ b/src/components/Spinner/style.module.scss @@ -1,5 +1,5 @@ .spin { - animation: spin 1s linear infinite; + animation: spin 5s linear infinite; } @keyframes spin { diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index ca8ea6d..a393824 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -199,17 +199,16 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { } }) + signers + .filter((s) => !parsedSignatureEventsMap.has(s)) + .forEach((s) => signerStatusMap.set(s, SignStatus.Pending)) + + // Get the first signer that hasn't signed 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)) From 1c3d3ca88ff951a83bb1689384ea807733c0e14c Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 14:44:11 +0200 Subject: [PATCH 10/10] refactor: pass meta to UserDetails instead of individual props --- src/components/UsersDetails.tsx/index.tsx | 41 ++++++++++------------- src/pages/verify/index.tsx | 29 ++-------------- 2 files changed, 20 insertions(+), 50 deletions(-) diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx index ea58f68..fc7d43d 100644 --- a/src/components/UsersDetails.tsx/index.tsx +++ b/src/components/UsersDetails.tsx/index.tsx @@ -10,7 +10,7 @@ import { SignStatus } from '../../utils' import { UserAvatar } from '../UserAvatar' -import { FlatMeta } from '../../hooks/useSigitMeta' +import { useSigitMeta } from '../../hooks/useSigitMeta' import { UserAvatarGroup } from '../UserAvatarGroup' import styles from './style.module.scss' @@ -28,31 +28,24 @@ import { useSelector } from 'react-redux' import { State } from '../../store/rootReducer' import { TooltipChild } from '../TooltipChild' import { DisplaySigner } from '../DisplaySigner' +import { Meta } from '../../types' -type UsersDetailsProps = Pick< - FlatMeta, - | 'submittedBy' - | 'signers' - | 'viewers' - | 'fileHashes' - | 'parsedSignatureEvents' - | 'createdAt' - | 'signedStatus' - | 'completedAt' - | 'signersStatus' -> +interface UsersDetailsProps { + meta: Meta +} -export const UsersDetails = ({ - submittedBy, - signers, - viewers, - fileHashes, - parsedSignatureEvents, - createdAt, - signedStatus, - completedAt, - signersStatus -}: UsersDetailsProps) => { +export const UsersDetails = ({ meta }: UsersDetailsProps) => { + const { + submittedBy, + signers, + viewers, + fileHashes, + signersStatus, + createdAt, + completedAt, + parsedSignatureEvents, + signedStatus + } = useSigitMeta(meta) const { usersPubkey } = useSelector((state: State) => state.auth) const profiles = useSigitProfiles([ ...(submittedBy ? [submittedBy] : []), diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index bd35f14..d991666 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -54,19 +54,8 @@ export const VerifyPage = () => { */ const { uploadedZip, meta } = location.state || {} - const { - submittedBy, - zipUrl, - encryptionKey, - signers, - viewers, - fileHashes, - parsedSignatureEvents, - createdAt, - signedStatus, - completedAt, - signersStatus - } = useSigitMeta(meta) + const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } = + useSigitMeta(meta) const profiles = useSigitProfiles([ ...(submittedBy ? [submittedBy] : []), @@ -470,19 +459,7 @@ export const VerifyPage = () => { } - right={ - - } + right={} /> )}