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
+}