sigit.io/src/pages/sign/index.tsx

855 lines
22 KiB
TypeScript
Raw Normal View History

2024-05-14 09:27:05 +00:00
import {
Box,
Button,
2024-05-22 06:19:40 +00:00
IconButton,
2024-05-14 09:27:05 +00:00
List,
ListItem,
ListSubheader,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TextField,
2024-05-22 06:19:40 +00:00
Tooltip,
2024-05-14 09:27:05 +00:00
Typography,
useTheme
} from '@mui/material'
import axios from 'axios'
import saveAs from 'file-saver'
import JSZip from 'jszip'
import _ from 'lodash'
import { MuiFileInput } from 'mui-file-input'
2024-05-22 06:19:40 +00:00
import { Event, verifyEvent } from 'nostr-tools'
2024-05-14 09:27:05 +00:00
import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
2024-05-16 11:22:05 +00:00
import { useNavigate, useSearchParams } from 'react-router-dom'
2024-05-14 09:27:05 +00:00
import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner'
2024-05-16 11:22:05 +00:00
import { UserComponent } from '../../components/username'
2024-05-14 09:27:05 +00:00
import { MetadataController, NostrController } from '../../controllers'
2024-05-16 11:22:05 +00:00
import { appPrivateRoutes } from '../../routes'
2024-05-14 09:27:05 +00:00
import { State } from '../../store/rootReducer'
2024-05-22 06:19:40 +00:00
import {
CreateSignatureEventContent,
Meta,
ProfileMetadata,
User,
UserRole
} from '../../types'
2024-05-14 09:27:05 +00:00
import {
decryptArrayBuffer,
encryptArrayBuffer,
generateEncryptionKey,
getHash,
hexToNpub,
parseJson,
npubToHex,
2024-05-14 09:27:05 +00:00
readContentOfZipEntry,
sendDM,
shorten,
signEventForMetaFile,
uploadToFileStorage
} from '../../utils'
import styles from './style.module.scss'
2024-05-22 06:19:40 +00:00
import { Download } from '@mui/icons-material'
2024-05-14 09:27:05 +00:00
enum SignedStatus {
Fully_Signed,
User_Is_Next_Signer,
User_Is_Not_Next_Signer
}
2024-05-15 11:11:57 +00:00
export const SignPage = () => {
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
2024-05-14 09:27:05 +00:00
const [displayInput, setDisplayInput] = useState(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [encryptionKey, setEncryptionKey] = useState('')
const [zip, setZip] = useState<JSZip>()
const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [meta, setMeta] = useState<Meta | null>(null)
const [signedStatus, setSignedStatus] = useState<SignedStatus>()
2024-05-22 06:19:40 +00:00
const [submittedBy, setSubmittedBy] = useState<string>()
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
}>({})
const [signedBy, setSignedBy] = useState<`npub1${string}`[]>([])
2024-05-14 09:27:05 +00:00
const [nextSinger, setNextSinger] = useState<string>()
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
const [authUrl, setAuthUrl] = useState<string>()
const nostrController = NostrController.getInstance()
useEffect(() => {
2024-05-22 06:19:40 +00:00
if (zip) {
const generateCurrentFileHashes = async () => {
const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files)
.filter((entry) => entry.name.startsWith('files/') && !entry.dir)
.map((entry) => entry.name)
// generate hashes for all entries in files folder of zipArchive
// these hashes can be used to verify the originality of files
for (const fileName of fileNames) {
const arrayBuffer = await readContentOfZipEntry(
zip,
fileName,
'arraybuffer'
)
2024-05-14 09:27:05 +00:00
2024-05-22 06:19:40 +00:00
if (arrayBuffer) {
const hash = await getHash(arrayBuffer)
if (hash) {
fileHashes[fileName.replace(/^files\//, '')] = hash
2024-05-14 09:27:05 +00:00
}
2024-05-22 06:19:40 +00:00
} else {
fileHashes[fileName.replace(/^files\//, '')] = null
2024-05-14 09:27:05 +00:00
}
}
2024-05-22 06:19:40 +00:00
setCurrentFileHashes(fileHashes)
}
generateCurrentFileHashes()
}
}, [zip])
useEffect(() => {
if (signers.length > 0) {
// check if all signers have signed then its fully signed
if (signers.every((signer) => signedBy.includes(signer))) {
2024-05-14 09:27:05 +00:00
setSignedStatus(SignedStatus.Fully_Signed)
2024-05-22 06:19:40 +00:00
} else {
for (const signer of signers) {
if (!signedBy.includes(signer)) {
// signers in meta.json are in npub1 format
// so, convert it to hex before setting to nextSigner
setNextSinger(npubToHex(signer)!)
const usersNpub = hexToNpub(usersPubkey!)
if (signer === usersNpub) {
// logged in user is the next signer
setSignedStatus(SignedStatus.User_Is_Next_Signer)
} else {
setSignedStatus(SignedStatus.User_Is_Not_Next_Signer)
}
break
}
}
2024-05-14 09:27:05 +00:00
}
2024-05-22 06:19:40 +00:00
} else {
// there's no signer just viewers. So its fully signed
setSignedStatus(SignedStatus.Fully_Signed)
2024-05-14 09:27:05 +00:00
}
2024-05-22 06:19:40 +00:00
}, [signers, signedBy, usersPubkey])
2024-05-14 09:27:05 +00:00
useEffect(() => {
const fileUrl = searchParams.get('file')
const key = searchParams.get('key')
if (fileUrl && key) {
setIsLoading(true)
setLoadingSpinnerDesc('Fetching file from file server')
axios
.get(fileUrl, {
responseType: 'arraybuffer'
})
.then((res) => {
const fileName = fileUrl.split('/').pop()
const file = new File([res.data], fileName!)
decrypt(file, decodeURIComponent(key)).then((arrayBuffer) => {
2024-05-14 09:27:05 +00:00
if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
})
})
.catch((err) => {
console.error(`error occurred in getting file from ${fileUrl}`, err)
toast.error(
err.message || `error occurred in getting file from ${fileUrl}`
)
})
.finally(() => {
setIsLoading(false)
})
} else {
setIsLoading(false)
setDisplayInput(true)
}
}, [searchParams])
const decrypt = async (file: File, key: string) => {
setLoadingSpinnerDesc('Decrypting file')
const encryptedArrayBuffer = await file.arrayBuffer()
const arrayBuffer = await decryptArrayBuffer(encryptedArrayBuffer, key)
.catch((err) => {
console.log('err in decryption:>> ', err)
toast.error(err.message || 'An error occurred in decrypting file.')
return null
})
.finally(() => {
setIsLoading(false)
})
return arrayBuffer
}
const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => {
const decryptedZipFile = new File([arrayBuffer], 'decrypted.zip')
setLoadingSpinnerDesc('Parsing zip file')
const zip = await JSZip.loadAsync(decryptedZipFile).catch((err) => {
console.log('err in loading zip file :>> ', err)
toast.error(err.message || 'An error occurred in loading zip file.')
return null
})
if (!zip) return
setZip(zip)
setLoadingSpinnerDesc('Parsing meta.json')
const metaFileContent = await readContentOfZipEntry(
zip,
'meta.json',
'string'
)
if (!metaFileContent) {
setIsLoading(false)
return
}
const parsedMetaJson = await parseJson<Meta>(metaFileContent).catch(
(err) => {
console.log('err in parsing the content of meta.json :>> ', err)
toast.error(
err.message || 'error occurred in parsing the content of meta.json'
)
setIsLoading(false)
return null
}
)
2024-05-22 06:19:40 +00:00
if (!parsedMetaJson) return
const createSignatureEvent = await parseJson<Event>(
parsedMetaJson.createSignature
).catch((err) => {
console.log('err in parsing the createSignature event:>> ', err)
toast.error(
err.message || 'error occurred in parsing the create signature event'
)
setIsLoading(false)
return null
})
if (!createSignatureEvent) return
const isValidCreateSignature = verifyEvent(createSignatureEvent)
if (!isValidCreateSignature) {
toast.error('Create signature is invalid')
setIsLoading(false)
return
}
const createSignatureContent = await parseJson<CreateSignatureEventContent>(
createSignatureEvent.content
).catch((err) => {
console.log(
`err in parsing the createSignature event's content :>> `,
err
)
toast.error(
err.message ||
`error occurred in parsing the create signature event's content`
)
setIsLoading(false)
return null
})
if (!createSignatureContent) return
setSigners(createSignatureContent.signers)
setViewers(createSignatureContent.viewers)
setCreatorFileHashes(createSignatureContent.fileHashes)
setSubmittedBy(createSignatureEvent.pubkey)
setSignedBy(Object.keys(parsedMetaJson.docSignatures) as `npub1${string}`[])
2024-05-14 09:27:05 +00:00
setMeta(parsedMetaJson)
}
const handleDecrypt = async () => {
if (!selectedFile || !encryptionKey) return
setIsLoading(true)
const arrayBuffer = await decrypt(
selectedFile,
decodeURIComponent(encryptionKey)
)
2024-05-14 09:27:05 +00:00
if (!arrayBuffer) return
handleDecryptedArrayBuffer(arrayBuffer)
}
const handleSign = async () => {
if (!zip || !meta) return
setIsLoading(true)
setLoadingSpinnerDesc('parsing hashes.json file')
const hashesFileContent = await readContentOfZipEntry(
zip,
'hashes.json',
'string'
)
if (!hashesFileContent) {
setIsLoading(false)
return
}
let hashes = await parseJson(hashesFileContent).catch((err) => {
console.log('err in parsing the content of hashes.json :>> ', err)
toast.error(
err.message || 'error occurred in parsing the content of hashes.json'
)
setIsLoading(false)
return null
})
if (!hashes) return
setLoadingSpinnerDesc('Generating hashes for files')
setLoadingSpinnerDesc('Signing nostr event')
const signedEvent = await signEventForMetaFile(
2024-05-22 06:19:40 +00:00
JSON.stringify({
fileHashes: currentFileHashes
}),
2024-05-14 09:27:05 +00:00
nostrController,
setIsLoading
)
if (!signedEvent) return
const metaCopy = _.cloneDeep(meta)
2024-05-22 06:19:40 +00:00
metaCopy.docSignatures = {
...metaCopy.docSignatures,
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
2024-05-14 09:27:05 +00:00
}
2024-05-14 11:35:21 +00:00
const stringifiedMeta = JSON.stringify(metaCopy, null, 2)
2024-05-14 09:27:05 +00:00
zip.file('meta.json', stringifiedMeta)
const metaHash = await getHash(stringifiedMeta)
if (!metaHash) return
hashes = {
...hashes,
[usersPubkey!]: metaHash
}
zip.file('hashes.json', JSON.stringify(hashes, null, 2))
const arrayBuffer = await zip
.generateAsync({
type: 'arraybuffer',
compression: 'DEFLATE',
compressionOptions: {
level: 6
}
})
.catch((err) => {
console.log('err in zip:>> ', err)
setIsLoading(false)
toast.error(err.message || 'Error occurred in generating zip file')
return null
})
if (!arrayBuffer) return
const key = await generateEncryptionKey()
2024-05-14 09:27:05 +00:00
setLoadingSpinnerDesc('Encrypting zip file')
const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key)
2024-05-14 09:27:05 +00:00
const blob = new Blob([encryptedArrayBuffer])
setLoadingSpinnerDesc('Uploading zip file to file storage.')
const fileUrl = await uploadToFileStorage(blob, nostrController)
.then((url) => {
toast.success('zip file uploaded to file storage')
return url
})
.catch((err) => {
console.log('err in upload:>> ', err)
setIsLoading(false)
toast.error(err.message || 'Error occurred in uploading zip file')
return null
})
if (!fileUrl) return
// check if the current user is the last signer
const usersNpub = hexToNpub(usersPubkey!)
2024-05-22 06:19:40 +00:00
const lastSignerIndex = signers.length - 1
const signerIndex = signers.indexOf(usersNpub)
2024-05-14 09:27:05 +00:00
const isLastSigner = signerIndex === lastSignerIndex
// if current user is the last signer, then send DMs to all signers and viewers
if (isLastSigner) {
const userSet = new Set<`npub1${string}`>()
2024-05-14 09:27:05 +00:00
2024-05-22 06:19:40 +00:00
if (submittedBy) {
userSet.add(hexToNpub(submittedBy))
}
2024-05-14 09:27:05 +00:00
2024-05-22 06:19:40 +00:00
signers.forEach((signer) => {
2024-05-14 09:27:05 +00:00
userSet.add(signer)
})
2024-05-22 06:19:40 +00:00
viewers.forEach((viewer) => {
2024-05-14 09:27:05 +00:00
userSet.add(viewer)
})
const users = Array.from(userSet)
for (const user of users) {
// todo: execute in parallel
await sendDM(
fileUrl,
key,
npubToHex(user)!,
2024-05-14 09:27:05 +00:00
nostrController,
false,
setAuthUrl
)
}
} else {
2024-05-22 06:19:40 +00:00
const nextSigner = signers[signerIndex + 1]
2024-05-14 09:27:05 +00:00
await sendDM(
fileUrl,
key,
npubToHex(nextSigner)!,
2024-05-14 09:27:05 +00:00
nostrController,
false,
setAuthUrl
)
}
setIsLoading(false)
// update search params with updated file url and encryption key
setSearchParams({
file: fileUrl,
key: key
})
2024-05-14 09:27:05 +00:00
}
const handleExport = async () => {
if (!meta || !zip || !usersPubkey) return
const usersNpub = hexToNpub(usersPubkey)
2024-05-14 09:27:05 +00:00
if (
2024-05-22 06:19:40 +00:00
!signers.includes(usersNpub) &&
!viewers.includes(usersNpub) &&
submittedBy !== usersNpub
2024-05-14 09:27:05 +00:00
)
return
setIsLoading(true)
setLoadingSpinnerDesc('Signing nostr event')
2024-05-22 06:19:40 +00:00
const signedEvent = await signEventForMetaFile(
JSON.stringify({
fileHashes: currentFileHashes
}),
nostrController,
setIsLoading
)
2024-05-14 09:27:05 +00:00
if (!signedEvent) return
const exportSignature = JSON.stringify(signedEvent, null, 2)
const stringifiedMeta = JSON.stringify(
{
...meta,
exportSignature
},
null,
2
)
zip.file('meta.json', stringifiedMeta)
const arrayBuffer = await zip
.generateAsync({
type: 'arraybuffer',
compression: 'DEFLATE',
compressionOptions: {
level: 6
}
})
.catch((err) => {
console.log('err in zip:>> ', err)
setIsLoading(false)
toast.error(err.message || 'Error occurred in generating zip file')
return null
})
if (!arrayBuffer) return
const blob = new Blob([arrayBuffer])
saveAs(blob, 'exported.zip')
setIsLoading(false)
navigate(appPrivateRoutes.verify)
2024-05-14 09:27:05 +00:00
}
if (authUrl) {
return (
<iframe
2024-05-16 06:25:30 +00:00
title="Nsecbunker auth"
2024-05-14 09:27:05 +00:00
src={authUrl}
2024-05-16 06:25:30 +00:00
width="100%"
height="500px"
2024-05-14 09:27:05 +00:00
/>
)
}
return (
<>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
<Box className={styles.container}>
{displayInput && (
<>
2024-05-16 06:25:30 +00:00
<Typography component="label" variant="h6">
2024-05-14 09:27:05 +00:00
Select sigit file
</Typography>
2024-05-15 06:19:28 +00:00
<Box className={styles.inputBlock}>
2024-05-14 09:27:05 +00:00
<MuiFileInput
2024-05-16 06:25:30 +00:00
placeholder="Select file"
2024-05-14 09:27:05 +00:00
value={selectedFile}
onChange={(value) => setSelectedFile(value)}
/>
{selectedFile && (
<TextField
2024-05-16 06:25:30 +00:00
label="Encryption Key"
variant="outlined"
2024-05-14 09:27:05 +00:00
value={encryptionKey}
onChange={(e) => setEncryptionKey(e.target.value)}
/>
)}
</Box>
{selectedFile && encryptionKey && (
2024-05-15 06:19:28 +00:00
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
2024-05-16 06:25:30 +00:00
<Button onClick={handleDecrypt} variant="contained">
2024-05-14 09:27:05 +00:00
Decrypt
</Button>
</Box>
)}
</>
)}
2024-05-22 06:19:40 +00:00
{submittedBy && zip && (
2024-05-14 09:27:05 +00:00
<>
2024-05-22 06:19:40 +00:00
<DisplayMeta
zip={zip}
submittedBy={submittedBy}
signers={signers}
viewers={viewers}
creatorFileHashes={creatorFileHashes}
currentFileHashes={currentFileHashes}
signedBy={signedBy}
nextSigner={nextSinger}
/>
{signedStatus === SignedStatus.Fully_Signed && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExport} variant="contained">
Export
</Button>
</Box>
)}
2024-05-14 09:27:05 +00:00
2024-05-22 06:19:40 +00:00
{signedStatus === SignedStatus.User_Is_Next_Signer && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleSign} variant="contained">
Sign
</Button>
</Box>
)}
2024-05-14 09:27:05 +00:00
</>
)}
</Box>
</>
)
}
type DisplayMetaProps = {
2024-05-22 06:19:40 +00:00
zip: JSZip
submittedBy: string
signers: `npub1${string}`[]
viewers: `npub1${string}`[]
creatorFileHashes: { [key: string]: string }
currentFileHashes: { [key: string]: string | null }
signedBy: `npub1${string}`[]
2024-05-14 09:27:05 +00:00
nextSigner?: string
}
2024-05-22 06:19:40 +00:00
const DisplayMeta = ({
zip,
submittedBy,
signers,
viewers,
creatorFileHashes,
currentFileHashes,
signedBy,
nextSigner
}: DisplayMetaProps) => {
2024-05-14 09:27:05 +00:00
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(() => {
2024-05-22 06:19:40 +00:00
signers.forEach((signer) => {
const hexKey = npubToHex(signer)
2024-05-14 09:27:05 +00:00
setUsers((prev) => {
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
2024-05-14 09:27:05 +00:00
return [
...prev,
{
pubkey: hexKey!,
2024-05-14 10:37:55 +00:00
role: UserRole.signer
2024-05-14 09:27:05 +00:00
}
]
})
})
2024-05-22 06:19:40 +00:00
viewers.forEach((viewer) => {
const hexKey = npubToHex(viewer)
2024-05-14 09:27:05 +00:00
setUsers((prev) => {
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
2024-05-14 09:27:05 +00:00
return [
...prev,
{
pubkey: hexKey!,
2024-05-14 10:37:55 +00:00
role: UserRole.viewer
2024-05-14 09:27:05 +00:00
}
]
})
})
2024-05-22 06:19:40 +00:00
}, [signers, viewers])
2024-05-14 09:27:05 +00:00
useEffect(() => {
const metadataController = new MetadataController()
const hexKeys: string[] = [
2024-05-22 06:19:40 +00:00
npubToHex(submittedBy)!,
...users.map((user) => user.pubkey)
]
2024-05-14 09:27:05 +00:00
hexKeys.forEach((key) => {
if (!(key in metadata)) {
2024-05-14 09:27:05 +00:00
metadataController
.findMetadata(key)
2024-05-14 09:27:05 +00:00
.then((metadataEvent) => {
const metadataContent =
metadataController.extractProfileMetadataContent(metadataEvent)
2024-05-14 09:27:05 +00:00
if (metadataContent)
setMetadata((prev) => ({
...prev,
[key]: metadataContent
2024-05-14 09:27:05 +00:00
}))
})
.catch((err) => {
console.error(`error occurred in finding metadata for: ${key}`, err)
2024-05-14 09:27:05 +00:00
})
}
})
2024-05-22 06:19:40 +00:00
}, [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)
}
2024-05-14 09:27:05 +00:00
return (
<List
sx={{
bgcolor: 'background.paper',
marginTop: 2
}}
subheader={
2024-05-22 06:19:40 +00:00
<ListSubheader className={styles.subHeader}>Meta Info</ListSubheader>
2024-05-14 09:27:05 +00:00
}
>
<ListItem
sx={{
marginTop: 1,
gap: '15px'
}}
>
2024-05-16 06:25:30 +00:00
<Typography variant="h6" sx={{ color: textColor }}>
2024-05-14 09:27:05 +00:00
Submitted By
</Typography>
{(function () {
2024-05-22 06:19:40 +00:00
const profile = metadata[submittedBy]
return (
<UserComponent
2024-05-22 06:19:40 +00:00
pubkey={submittedBy}
name={
2024-05-22 06:19:40 +00:00
profile?.display_name || profile?.name || shorten(submittedBy)
}
2024-05-22 06:19:40 +00:00
image={profile?.picture}
/>
)
})()}
2024-05-14 09:27:05 +00:00
</ListItem>
<ListItem
sx={{
marginTop: 1,
flexDirection: 'column',
alignItems: 'flex-start'
}}
>
2024-05-16 06:25:30 +00:00
<Typography variant="h6" sx={{ color: textColor }}>
2024-05-14 09:27:05 +00:00
Files
</Typography>
2024-05-22 06:19:40 +00:00
<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>
<Typography
component="label"
sx={{
color: isValidHash
? theme.palette.success.light
: theme.palette.error.main
}}
>
{isValidHash ? 'Valid' : 'Invalid'} hash
</Typography>
</Box>
)
})}
</Box>
2024-05-14 09:27:05 +00:00
</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, index) => {
const userMeta = metadata[user.pubkey]
let signedStatus = '-'
2024-05-14 12:14:53 +00:00
if (user.role === UserRole.signer) {
2024-05-14 09:27:05 +00:00
// check if user has signed the document
const usersNpub = hexToNpub(user.pubkey)
2024-05-22 06:19:40 +00:00
if (signedBy.includes(usersNpub)) {
2024-05-14 09:27:05 +00:00
signedStatus = 'Signed'
}
// check if user is the next signer
else if (user.pubkey === nextSigner) {
2024-05-14 12:14:53 +00:00
signedStatus = 'Awaiting Signature'
2024-05-14 09:27:05 +00:00
}
}
return (
<TableRow key={index}>
<TableCell className={styles.tableCell}>
2024-05-16 11:22:05 +00:00
<UserComponent
pubkey={user.pubkey}
name={
userMeta?.display_name ||
userMeta?.name ||
shorten(hexToNpub(user.pubkey))
}
image={userMeta?.picture}
/>
2024-05-14 09:27:05 +00:00
</TableCell>
<TableCell className={styles.tableCell}>
{user.role}
</TableCell>
<TableCell>{signedStatus}</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</ListItem>
</List>
)
}