In offline mode create a wrapper zip file #110
@ -1,67 +1,37 @@
|
|||||||
import {
|
import { Box, Button, Typography } from '@mui/material'
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListSubheader,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
useTheme
|
|
||||||
} from '@mui/material'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import saveAs from 'file-saver'
|
import saveAs from 'file-saver'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { MuiFileInput } from 'mui-file-input'
|
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 { useEffect, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { useNavigate, useSearchParams, useLocation } from 'react-router-dom'
|
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { UserComponent } from '../../components/username'
|
import CopyModal from '../../components/copyModal'
|
||||||
import { MetadataController, NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { appPublicRoutes } from '../../routes'
|
import { appPublicRoutes } from '../../routes'
|
||||||
import { State } from '../../store/rootReducer'
|
import { State } from '../../store/rootReducer'
|
||||||
import {
|
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
||||||
CreateSignatureEventContent,
|
|
||||||
Meta,
|
|
||||||
ProfileMetadata,
|
|
||||||
SignedEvent,
|
|
||||||
SignedEventContent,
|
|
||||||
User,
|
|
||||||
UserRole
|
|
||||||
} from '../../types'
|
|
||||||
import {
|
import {
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
encryptArrayBuffer,
|
encryptArrayBuffer,
|
||||||
generateEncryptionKey,
|
generateEncryptionKey,
|
||||||
|
generateKeysFile,
|
||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
parseJson,
|
isOnline,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
|
parseJson,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
sendDM,
|
sendDM,
|
||||||
shorten,
|
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
uploadToFileStorage,
|
uploadToFileStorage
|
||||||
isOnline,
|
|
||||||
generateKeysFile
|
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
import { DisplayMeta } from './internal/displayMeta'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import {
|
|
||||||
Cancel,
|
|
||||||
CheckCircle,
|
|
||||||
Download,
|
|
||||||
HourglassTop
|
|
||||||
} from '@mui/icons-material'
|
|
||||||
import CopyModal from '../../components/copyModal'
|
|
||||||
enum SignedStatus {
|
enum SignedStatus {
|
||||||
Fully_Signed,
|
Fully_Signed,
|
||||||
User_Is_Next_Signer,
|
User_Is_Next_Signer,
|
||||||
@ -967,386 +937,3 @@ export const SignPage = () => {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DisplayMetaProps = {
|
|
||||||
meta: Meta
|
|
||||||
zip: JSZip
|
|
||||||
submittedBy: string
|
|
||||||
signers: `npub1${string}`[]
|
|
||||||
viewers: `npub1${string}`[]
|
|
||||||
creatorFileHashes: { [key: string]: string }
|
|
||||||
currentFileHashes: { [key: string]: string | null }
|
|
||||||
signedBy: `npub1${string}`[]
|
|
||||||
nextSigner?: string
|
|
||||||
getPrevSignersSig: (usersNpub: string) => string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisplayMeta = ({
|
|
||||||
meta,
|
|
||||||
zip,
|
|
||||||
submittedBy,
|
|
||||||
signers,
|
|
||||||
viewers,
|
|
||||||
creatorFileHashes,
|
|
||||||
currentFileHashes,
|
|
||||||
signedBy,
|
|
||||||
nextSigner,
|
|
||||||
getPrevSignersSig
|
|
||||||
}: DisplayMetaProps) => {
|
|
||||||
const theme = useTheme()
|
|
||||||
|
|
||||||
const textColor = theme.palette.getContrastText(
|
|
||||||
theme.palette.background.paper
|
|
||||||
)
|
|
||||||
|
|
||||||
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
const [users, setUsers] = useState<User[]>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
signers.forEach((signer) => {
|
|
||||||
const hexKey = npubToHex(signer)
|
|
||||||
setUsers((prev) => {
|
|
||||||
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
|
||||||
|
|
||||||
return [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
pubkey: hexKey!,
|
|
||||||
role: UserRole.signer
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
viewers.forEach((viewer) => {
|
|
||||||
const hexKey = npubToHex(viewer)
|
|
||||||
setUsers((prev) => {
|
|
||||||
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
|
||||||
|
|
||||||
return [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
pubkey: hexKey!,
|
|
||||||
role: UserRole.viewer
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, [signers, viewers])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const metadataController = new MetadataController()
|
|
||||||
|
|
||||||
const hexKeys: string[] = [
|
|
||||||
npubToHex(submittedBy)!,
|
|
||||||
...users.map((user) => user.pubkey)
|
|
||||||
]
|
|
||||||
|
|
||||||
hexKeys.forEach((key) => {
|
|
||||||
if (!(key in metadata)) {
|
|
||||||
const handleMetadataEvent = (event: Event) => {
|
|
||||||
const metadataContent =
|
|
||||||
metadataController.extractProfileMetadataContent(event)
|
|
||||||
|
|
||||||
if (metadataContent)
|
|
||||||
setMetadata((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[key]: metadataContent
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
metadataController.on(key, (kind: number, event: Event) => {
|
|
||||||
if (kind === kinds.Metadata) {
|
|
||||||
handleMetadataEvent(event)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
metadataController
|
|
||||||
.findMetadata(key)
|
|
||||||
.then((metadataEvent) => {
|
|
||||||
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(`error occurred in finding metadata for: ${key}`, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [users, submittedBy])
|
|
||||||
|
|
||||||
const downloadFile = async (filename: string) => {
|
|
||||||
const arrayBuffer = await readContentOfZipEntry(
|
|
||||||
zip,
|
|
||||||
`files/${filename}`,
|
|
||||||
'arraybuffer'
|
|
||||||
)
|
|
||||||
if (!arrayBuffer) return
|
|
||||||
|
|
||||||
const blob = new Blob([arrayBuffer])
|
|
||||||
saveAs(blob, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<List
|
|
||||||
sx={{
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
marginTop: 2
|
|
||||||
}}
|
|
||||||
subheader={
|
|
||||||
<ListSubheader className={styles.subHeader}>Meta Info</ListSubheader>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ListItem
|
|
||||||
sx={{
|
|
||||||
marginTop: 1,
|
|
||||||
gap: '15px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6" sx={{ color: textColor }}>
|
|
||||||
Submitted By
|
|
||||||
</Typography>
|
|
||||||
{(function () {
|
|
||||||
const profile = metadata[submittedBy]
|
|
||||||
return (
|
|
||||||
<UserComponent
|
|
||||||
pubkey={submittedBy}
|
|
||||||
name={
|
|
||||||
profile?.display_name ||
|
|
||||||
profile?.name ||
|
|
||||||
shorten(hexToNpub(submittedBy))
|
|
||||||
}
|
|
||||||
image={profile?.picture}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
</ListItem>
|
|
||||||
<ListItem
|
|
||||||
sx={{
|
|
||||||
marginTop: 1,
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'flex-start'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6" sx={{ color: textColor }}>
|
|
||||||
Files
|
|
||||||
</Typography>
|
|
||||||
<Box className={styles.filesWrapper}>
|
|
||||||
{Object.entries(currentFileHashes).map(([filename, hash], index) => {
|
|
||||||
const isValidHash = creatorFileHashes[filename] === hash
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box key={`file-${index}`} className={styles.file}>
|
|
||||||
<Tooltip title="Download File" arrow>
|
|
||||||
<IconButton onClick={() => downloadFile(filename)}>
|
|
||||||
<Download />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Typography
|
|
||||||
component="label"
|
|
||||||
sx={{
|
|
||||||
color: textColor,
|
|
||||||
flexGrow: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filename}
|
|
||||||
</Typography>
|
|
||||||
{isValidHash && (
|
|
||||||
<Tooltip title="File integrity check passed" arrow>
|
|
||||||
<CheckCircle sx={{ color: theme.palette.success.light }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{!isValidHash && (
|
|
||||||
<Tooltip title="File integrity check failed" arrow>
|
|
||||||
<Cancel sx={{ color: theme.palette.error.main }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem sx={{ marginTop: 1 }}>
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell className={styles.tableCell}>User</TableCell>
|
|
||||||
<TableCell className={styles.tableCell}>Role</TableCell>
|
|
||||||
<TableCell>Signed Status</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{users.map((user) => (
|
|
||||||
<DisplayUser
|
|
||||||
key={user.pubkey}
|
|
||||||
meta={meta}
|
|
||||||
user={user}
|
|
||||||
metadata={metadata}
|
|
||||||
signedBy={signedBy}
|
|
||||||
nextSigner={nextSigner}
|
|
||||||
getPrevSignersSig={getPrevSignersSig}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PrevSignatureValidationEnum {
|
|
||||||
Pending,
|
|
||||||
Valid,
|
|
||||||
Invalid
|
|
||||||
}
|
|
||||||
|
|
||||||
enum UserStatus {
|
|
||||||
Viewer = 'Viewer',
|
|
||||||
Awaiting = 'Awaiting Signature',
|
|
||||||
Signed = 'Signed',
|
|
||||||
Pending = 'Pending'
|
|
||||||
}
|
|
||||||
|
|
||||||
type DisplayUserProps = {
|
|
||||||
meta: Meta
|
|
||||||
user: User
|
|
||||||
metadata: { [key: string]: ProfileMetadata }
|
|
||||||
signedBy: `npub1${string}`[]
|
|
||||||
nextSigner?: string
|
|
||||||
getPrevSignersSig: (usersNpub: string) => string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisplayUser = ({
|
|
||||||
meta,
|
|
||||||
user,
|
|
||||||
metadata,
|
|
||||||
signedBy,
|
|
||||||
nextSigner,
|
|
||||||
getPrevSignersSig
|
|
||||||
}: DisplayUserProps) => {
|
|
||||||
const theme = useTheme()
|
|
||||||
|
|
||||||
const userMeta = metadata[user.pubkey]
|
|
||||||
const [userStatus, setUserStatus] = useState<UserStatus>(UserStatus.Pending)
|
|
||||||
const [prevSignatureStatus, setPreviousSignatureStatus] =
|
|
||||||
useState<PrevSignatureValidationEnum>(PrevSignatureValidationEnum.Pending)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user.role === UserRole.viewer) {
|
|
||||||
setUserStatus(UserStatus.Viewer)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if user has signed the document
|
|
||||||
const usersNpub = hexToNpub(user.pubkey)
|
|
||||||
if (signedBy.includes(usersNpub)) {
|
|
||||||
setUserStatus(UserStatus.Signed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if user is the next signer
|
|
||||||
if (user.pubkey === nextSigner) {
|
|
||||||
setUserStatus(UserStatus.Awaiting)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}, [user, nextSigner, signedBy])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const validatePrevSignature = async () => {
|
|
||||||
const handleNullCase = () => {
|
|
||||||
setPreviousSignatureStatus(PrevSignatureValidationEnum.Invalid)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get previous signers sig from the content of current signers signed event
|
|
||||||
const npub = hexToNpub(user.pubkey)
|
|
||||||
const signedEvent = await parseJson<Event>(
|
|
||||||
meta.docSignatures[npub]
|
|
||||||
).catch((err) => {
|
|
||||||
console.log(`err in parsing the singed event for ${npub}:>> `, err)
|
|
||||||
toast.error(
|
|
||||||
err.message ||
|
|
||||||
'error occurred in parsing the signed event signature event'
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!signedEvent) return handleNullCase()
|
|
||||||
|
|
||||||
// now that we have signed event of current signer, we'll extract prevSig from its content
|
|
||||||
const parsedContent = await parseJson<SignedEventContent>(
|
|
||||||
signedEvent.content
|
|
||||||
).catch((err) => {
|
|
||||||
console.log(
|
|
||||||
`an error occurred in parsing the content of signedEvent of ${npub}`,
|
|
||||||
err
|
|
||||||
)
|
|
||||||
toast.error(
|
|
||||||
err.message ||
|
|
||||||
`an error occurred in parsing the content of signedEvent of ${npub}`
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!parsedContent) return handleNullCase()
|
|
||||||
|
|
||||||
const prevSignersSignature = getPrevSignersSig(npub)
|
|
||||||
|
|
||||||
if (!prevSignersSignature) return handleNullCase()
|
|
||||||
|
|
||||||
setPreviousSignatureStatus(
|
|
||||||
parsedContent.prevSig === prevSignersSignature
|
|
||||||
? PrevSignatureValidationEnum.Valid
|
|
||||||
: PrevSignatureValidationEnum.Invalid
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userStatus === UserStatus.Signed) {
|
|
||||||
validatePrevSignature()
|
|
||||||
}
|
|
||||||
}, [userStatus, meta.docSignatures, user.pubkey, getPrevSignersSig])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell className={styles.tableCell}>
|
|
||||||
<UserComponent
|
|
||||||
pubkey={user.pubkey}
|
|
||||||
name={
|
|
||||||
userMeta?.display_name ||
|
|
||||||
userMeta?.name ||
|
|
||||||
shorten(hexToNpub(user.pubkey))
|
|
||||||
}
|
|
||||||
image={userMeta?.picture}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={styles.tableCell}>{user.role}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
||||||
<Typography component="label">{userStatus}</Typography>
|
|
||||||
{userStatus === UserStatus.Signed && (
|
|
||||||
<>
|
|
||||||
{prevSignatureStatus === PrevSignatureValidationEnum.Valid && (
|
|
||||||
<Tooltip title="Contains valid signature of prev signer" arrow>
|
|
||||||
<CheckCircle sx={{ color: theme.palette.success.light }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{prevSignatureStatus === PrevSignatureValidationEnum.Invalid && (
|
|
||||||
<Tooltip
|
|
||||||
title="Contains invalid signature of prev signer"
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<Cancel sx={{ color: theme.palette.error.main }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{userStatus === UserStatus.Awaiting && (
|
|
||||||
<Tooltip title="Waiting for user's sign" arrow>
|
|
||||||
<HourglassTop />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
426
src/pages/sign/internal/displayMeta.tsx
Normal file
426
src/pages/sign/internal/displayMeta.tsx
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
import JSZip from 'jszip'
|
||||||
s marked this conversation as resolved
|
|||||||
|
import {
|
||||||
|
Meta,
|
||||||
|
ProfileMetadata,
|
||||||
|
SignedEventContent,
|
||||||
|
User,
|
||||||
|
UserRole
|
||||||
|
} from '../../../types'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListSubheader,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material'
|
||||||
|
import {
|
||||||
|
Download,
|
||||||
|
CheckCircle,
|
||||||
|
Cancel,
|
||||||
|
HourglassTop
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
import saveAs from 'file-saver'
|
||||||
|
import { kinds, Event } from 'nostr-tools'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { UserComponent } from '../../../components/username'
|
||||||
|
import { MetadataController } from '../../../controllers'
|
||||||
|
import {
|
||||||
|
npubToHex,
|
||||||
|
readContentOfZipEntry,
|
||||||
|
shorten,
|
||||||
|
hexToNpub,
|
||||||
|
parseJson
|
||||||
|
} from '../../../utils'
|
||||||
|
import styles from '../style.module.scss'
|
||||||
|
|
||||||
|
type DisplayMetaProps = {
|
||||||
|
meta: Meta
|
||||||
|
zip: JSZip
|
||||||
|
submittedBy: string
|
||||||
|
signers: `npub1${string}`[]
|
||||||
|
viewers: `npub1${string}`[]
|
||||||
|
creatorFileHashes: { [key: string]: string }
|
||||||
|
currentFileHashes: { [key: string]: string | null }
|
||||||
|
signedBy: `npub1${string}`[]
|
||||||
|
nextSigner?: string
|
||||||
|
getPrevSignersSig: (usersNpub: string) => string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplayMeta = ({
|
||||||
|
meta,
|
||||||
|
zip,
|
||||||
|
submittedBy,
|
||||||
|
signers,
|
||||||
|
viewers,
|
||||||
|
creatorFileHashes,
|
||||||
|
currentFileHashes,
|
||||||
|
signedBy,
|
||||||
|
nextSigner,
|
||||||
|
getPrevSignersSig
|
||||||
|
}: DisplayMetaProps) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
const textColor = theme.palette.getContrastText(
|
||||||
|
theme.palette.background.paper
|
||||||
|
)
|
||||||
|
|
||||||
|
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
const [users, setUsers] = useState<User[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
signers.forEach((signer) => {
|
||||||
|
const hexKey = npubToHex(signer)
|
||||||
|
setUsers((prev) => {
|
||||||
|
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
||||||
|
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
pubkey: hexKey!,
|
||||||
|
role: UserRole.signer
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
viewers.forEach((viewer) => {
|
||||||
|
const hexKey = npubToHex(viewer)
|
||||||
|
setUsers((prev) => {
|
||||||
|
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
||||||
|
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
pubkey: hexKey!,
|
||||||
|
role: UserRole.viewer
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [signers, viewers])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
|
const hexKeys: string[] = [
|
||||||
|
npubToHex(submittedBy)!,
|
||||||
|
...users.map((user) => user.pubkey)
|
||||||
|
]
|
||||||
|
|
||||||
|
hexKeys.forEach((key) => {
|
||||||
|
if (!(key in metadata)) {
|
||||||
|
const handleMetadataEvent = (event: Event) => {
|
||||||
|
const metadataContent =
|
||||||
|
metadataController.extractProfileMetadataContent(event)
|
||||||
|
|
||||||
|
if (metadataContent)
|
||||||
|
setMetadata((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: metadataContent
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataController.on(key, (kind: number, event: Event) => {
|
||||||
|
if (kind === kinds.Metadata) {
|
||||||
|
handleMetadataEvent(event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
metadataController
|
||||||
|
.findMetadata(key)
|
||||||
|
.then((metadataEvent) => {
|
||||||
|
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`error occurred in finding metadata for: ${key}`, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [users, submittedBy])
|
||||||
|
|
||||||
|
const downloadFile = async (filename: string) => {
|
||||||
|
const arrayBuffer = await readContentOfZipEntry(
|
||||||
|
zip,
|
||||||
|
`files/${filename}`,
|
||||||
|
'arraybuffer'
|
||||||
|
)
|
||||||
|
if (!arrayBuffer) return
|
||||||
|
|
||||||
|
const blob = new Blob([arrayBuffer])
|
||||||
|
saveAs(blob, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
marginTop: 2
|
||||||
|
}}
|
||||||
|
subheader={
|
||||||
|
<ListSubheader className={styles.subHeader}>Meta Info</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItem
|
||||||
|
sx={{
|
||||||
|
marginTop: 1,
|
||||||
|
gap: '15px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ color: textColor }}>
|
||||||
|
Submitted By
|
||||||
|
</Typography>
|
||||||
|
{(function () {
|
||||||
|
const profile = metadata[submittedBy]
|
||||||
|
return (
|
||||||
|
<UserComponent
|
||||||
|
pubkey={submittedBy}
|
||||||
|
name={
|
||||||
|
profile?.display_name ||
|
||||||
|
profile?.name ||
|
||||||
|
shorten(hexToNpub(submittedBy))
|
||||||
|
}
|
||||||
|
image={profile?.picture}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
sx={{
|
||||||
|
marginTop: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-start'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ color: textColor }}>
|
||||||
|
Files
|
||||||
|
</Typography>
|
||||||
|
<Box className={styles.filesWrapper}>
|
||||||
|
{Object.entries(currentFileHashes).map(([filename, hash], index) => {
|
||||||
|
const isValidHash = creatorFileHashes[filename] === hash
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={`file-${index}`} className={styles.file}>
|
||||||
|
<Tooltip title="Download File" arrow>
|
||||||
|
<IconButton onClick={() => downloadFile(filename)}>
|
||||||
|
<Download />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Typography
|
||||||
|
component="label"
|
||||||
|
sx={{
|
||||||
|
color: textColor,
|
||||||
|
flexGrow: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filename}
|
||||||
|
</Typography>
|
||||||
|
{isValidHash && (
|
||||||
|
<Tooltip title="File integrity check passed" arrow>
|
||||||
|
<CheckCircle sx={{ color: theme.palette.success.light }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{!isValidHash && (
|
||||||
|
<Tooltip title="File integrity check failed" arrow>
|
||||||
|
<Cancel sx={{ color: theme.palette.error.main }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem sx={{ marginTop: 1 }}>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className={styles.tableCell}>User</TableCell>
|
||||||
|
<TableCell className={styles.tableCell}>Role</TableCell>
|
||||||
|
<TableCell>Signed Status</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{users.map((user) => (
|
||||||
|
<DisplayUser
|
||||||
|
key={user.pubkey}
|
||||||
|
meta={meta}
|
||||||
|
user={user}
|
||||||
|
metadata={metadata}
|
||||||
|
signedBy={signedBy}
|
||||||
|
nextSigner={nextSigner}
|
||||||
|
getPrevSignersSig={getPrevSignersSig}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PrevSignatureValidationEnum {
|
||||||
|
Pending,
|
||||||
|
Valid,
|
||||||
|
Invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UserStatus {
|
||||||
|
Viewer = 'Viewer',
|
||||||
|
Awaiting = 'Awaiting Signature',
|
||||||
|
Signed = 'Signed',
|
||||||
|
Pending = 'Pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
type DisplayUserProps = {
|
||||||
|
meta: Meta
|
||||||
|
user: User
|
||||||
|
metadata: { [key: string]: ProfileMetadata }
|
||||||
|
signedBy: `npub1${string}`[]
|
||||||
|
nextSigner?: string
|
||||||
|
getPrevSignersSig: (usersNpub: string) => string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const DisplayUser = ({
|
||||||
|
meta,
|
||||||
|
user,
|
||||||
|
metadata,
|
||||||
|
signedBy,
|
||||||
|
nextSigner,
|
||||||
|
getPrevSignersSig
|
||||||
|
}: DisplayUserProps) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
const userMeta = metadata[user.pubkey]
|
||||||
|
const [userStatus, setUserStatus] = useState<UserStatus>(UserStatus.Pending)
|
||||||
|
const [prevSignatureStatus, setPreviousSignatureStatus] =
|
||||||
|
useState<PrevSignatureValidationEnum>(PrevSignatureValidationEnum.Pending)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user.role === UserRole.viewer) {
|
||||||
|
setUserStatus(UserStatus.Viewer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user has signed the document
|
||||||
|
const usersNpub = hexToNpub(user.pubkey)
|
||||||
|
if (signedBy.includes(usersNpub)) {
|
||||||
|
setUserStatus(UserStatus.Signed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user is the next signer
|
||||||
|
if (user.pubkey === nextSigner) {
|
||||||
|
setUserStatus(UserStatus.Awaiting)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}, [user, nextSigner, signedBy])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const validatePrevSignature = async () => {
|
||||||
|
const handleNullCase = () => {
|
||||||
|
setPreviousSignatureStatus(PrevSignatureValidationEnum.Invalid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get previous signers sig from the content of current signers signed event
|
||||||
|
const npub = hexToNpub(user.pubkey)
|
||||||
|
const signedEvent = await parseJson<Event>(
|
||||||
|
meta.docSignatures[npub]
|
||||||
|
).catch((err) => {
|
||||||
|
console.log(`err in parsing the singed event for ${npub}:>> `, err)
|
||||||
|
toast.error(
|
||||||
|
err.message ||
|
||||||
|
'error occurred in parsing the signed event signature event'
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!signedEvent) return handleNullCase()
|
||||||
|
|
||||||
|
// now that we have signed event of current signer, we'll extract prevSig from its content
|
||||||
|
const parsedContent = await parseJson<SignedEventContent>(
|
||||||
|
signedEvent.content
|
||||||
|
).catch((err) => {
|
||||||
|
console.log(
|
||||||
|
`an error occurred in parsing the content of signedEvent of ${npub}`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
toast.error(
|
||||||
|
err.message ||
|
||||||
|
`an error occurred in parsing the content of signedEvent of ${npub}`
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!parsedContent) return handleNullCase()
|
||||||
|
|
||||||
|
const prevSignersSignature = getPrevSignersSig(npub)
|
||||||
|
|
||||||
|
if (!prevSignersSignature) return handleNullCase()
|
||||||
|
|
||||||
|
setPreviousSignatureStatus(
|
||||||
|
parsedContent.prevSig === prevSignersSignature
|
||||||
|
? PrevSignatureValidationEnum.Valid
|
||||||
|
: PrevSignatureValidationEnum.Invalid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userStatus === UserStatus.Signed) {
|
||||||
|
validatePrevSignature()
|
||||||
|
}
|
||||||
|
}, [userStatus, meta.docSignatures, user.pubkey, getPrevSignersSig])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className={styles.tableCell}>
|
||||||
|
<UserComponent
|
||||||
|
pubkey={user.pubkey}
|
||||||
|
name={
|
||||||
|
userMeta?.display_name ||
|
||||||
|
userMeta?.name ||
|
||||||
|
shorten(hexToNpub(user.pubkey))
|
||||||
|
}
|
||||||
|
image={userMeta?.picture}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className={styles.tableCell}>{user.role}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
<Typography component="label">{userStatus}</Typography>
|
||||||
|
{userStatus === UserStatus.Signed && (
|
||||||
|
<>
|
||||||
|
{prevSignatureStatus === PrevSignatureValidationEnum.Valid && (
|
||||||
|
<Tooltip title="Contains valid signature of prev signer" arrow>
|
||||||
|
<CheckCircle sx={{ color: theme.palette.success.light }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{prevSignatureStatus === PrevSignatureValidationEnum.Invalid && (
|
||||||
|
<Tooltip
|
||||||
|
title="Contains invalid signature of prev signer"
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Cancel sx={{ color: theme.palette.error.main }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{userStatus === UserStatus.Awaiting && (
|
||||||
|
<Tooltip title="Waiting for user's sign" arrow>
|
||||||
|
<HourglassTop />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user
pls add more comments