feat: improve verification process #65
@ -65,7 +65,9 @@ export const UserComponent = ({ pubkey, name, image }: UserProps) => {
|
|||||||
const roboImage = `https://robohash.org/${npub}.png?set=set3`
|
const roboImage = `https://robohash.org/${npub}.png?set=set3`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<Box
|
||||||
|
sx={{ display: 'flex', alignItems: 'center', gap: '10px', flexGrow: 1 }}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={image || roboImage}
|
src={image || roboImage}
|
||||||
alt="User Image"
|
alt="User Image"
|
||||||
|
@ -268,23 +268,22 @@ export const CreatePage = () => {
|
|||||||
const viewers = users.filter((user) => user.role === UserRole.viewer)
|
const viewers = users.filter((user) => user.role === UserRole.viewer)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
const signedEvent = await signEventForMetaFile(
|
const createSignature = await signEventForMetaFile(
|
||||||
fileHashes,
|
JSON.stringify({
|
||||||
|
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
|
||||||
|
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
|
||||||
|
fileHashes
|
||||||
|
}),
|
||||||
nostrController,
|
nostrController,
|
||||||
setIsLoading
|
setIsLoading
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!signedEvent) return
|
if (!createSignature) return
|
||||||
|
|
||||||
// create content for meta file
|
// create content for meta file
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
|
createSignature: JSON.stringify(createSignature, null, 2),
|
||||||
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
|
docSignatures: {}
|
||||||
fileHashes,
|
|
||||||
submittedBy: hexToNpub(usersPubkey!),
|
|
||||||
signedEvents: {
|
|
||||||
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
IconButton,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListSubheader,
|
ListSubheader,
|
||||||
@ -10,6 +11,7 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
TextField,
|
TextField,
|
||||||
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
useTheme
|
useTheme
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
@ -18,7 +20,7 @@ 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 { EventTemplate } 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 } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
@ -28,7 +30,13 @@ import { UserComponent } from '../../components/username'
|
|||||||
import { MetadataController, NostrController } from '../../controllers'
|
import { MetadataController, NostrController } from '../../controllers'
|
||||||
import { appPrivateRoutes } from '../../routes'
|
import { appPrivateRoutes } from '../../routes'
|
||||||
import { State } from '../../store/rootReducer'
|
import { State } from '../../store/rootReducer'
|
||||||
import { Meta, ProfileMetadata, User, UserRole } from '../../types'
|
import {
|
||||||
|
CreateSignatureEventContent,
|
||||||
|
Meta,
|
||||||
|
ProfileMetadata,
|
||||||
|
User,
|
||||||
|
UserRole
|
||||||
|
} from '../../types'
|
||||||
import {
|
import {
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
encryptArrayBuffer,
|
encryptArrayBuffer,
|
||||||
@ -44,6 +52,7 @@ import {
|
|||||||
uploadToFileStorage
|
uploadToFileStorage
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
import { Download } from '@mui/icons-material'
|
||||||
|
|
||||||
enum SignedStatus {
|
enum SignedStatus {
|
||||||
Fully_Signed,
|
Fully_Signed,
|
||||||
@ -68,6 +77,19 @@ export const SignPage = () => {
|
|||||||
const [meta, setMeta] = useState<Meta | null>(null)
|
const [meta, setMeta] = useState<Meta | null>(null)
|
||||||
const [signedStatus, setSignedStatus] = useState<SignedStatus>()
|
const [signedStatus, setSignedStatus] = useState<SignedStatus>()
|
||||||
|
|
||||||
|
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}`[]>([])
|
||||||
|
|
||||||
const [nextSinger, setNextSinger] = useState<string>()
|
const [nextSinger, setNextSinger] = useState<string>()
|
||||||
|
|
||||||
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||||
@ -76,42 +98,70 @@ export const SignPage = () => {
|
|||||||
const nostrController = NostrController.getInstance()
|
const nostrController = NostrController.getInstance()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (meta) {
|
if (zip) {
|
||||||
setDisplayInput(false)
|
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)
|
||||||
|
|
||||||
// get list of users who have signed
|
// generate hashes for all entries in files folder of zipArchive
|
||||||
const signedBy = Object.keys(meta.signedEvents)
|
// these hashes can be used to verify the originality of files
|
||||||
|
for (const fileName of fileNames) {
|
||||||
|
const arrayBuffer = await readContentOfZipEntry(
|
||||||
|
zip,
|
||||||
|
fileName,
|
||||||
|
'arraybuffer'
|
||||||
|
)
|
||||||
|
|
||||||
if (meta.signers.length > 0) {
|
if (arrayBuffer) {
|
||||||
// check if all signers have signed then its fully signed
|
const hash = await getHash(arrayBuffer)
|
||||||
if (meta.signers.every((signer) => signedBy.includes(signer))) {
|
|
||||||
setSignedStatus(SignedStatus.Fully_Signed)
|
|
||||||
} else {
|
|
||||||
for (const signer of meta.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 (hash) {
|
||||||
|
fileHashes[fileName.replace(/^files\//, '')] = hash
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
fileHashes[fileName.replace(/^files\//, '')] = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// there's no signer just viewers. So its fully signed
|
setCurrentFileHashes(fileHashes)
|
||||||
setSignedStatus(SignedStatus.Fully_Signed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateCurrentFileHashes()
|
||||||
}
|
}
|
||||||
}, [meta, usersPubkey])
|
}, [zip])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (signers.length > 0) {
|
||||||
|
// check if all signers have signed then its fully signed
|
||||||
|
if (signers.every((signer) => signedBy.includes(signer))) {
|
||||||
|
setSignedStatus(SignedStatus.Fully_Signed)
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// there's no signer just viewers. So its fully signed
|
||||||
|
setSignedStatus(SignedStatus.Fully_Signed)
|
||||||
|
}
|
||||||
|
}, [signers, signedBy, usersPubkey])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fileUrl = searchParams.get('file')
|
const fileUrl = searchParams.get('file')
|
||||||
@ -205,6 +255,53 @@ export const SignPage = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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}`[])
|
||||||
|
|
||||||
setMeta(parsedMetaJson)
|
setMeta(parsedMetaJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,36 +349,11 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
setLoadingSpinnerDesc('Generating hashes for files')
|
setLoadingSpinnerDesc('Generating hashes for files')
|
||||||
|
|
||||||
const fileHashes: { [key: string]: string } = {}
|
|
||||||
const fileNames = Object.keys(meta.fileHashes)
|
|
||||||
|
|
||||||
// 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 filePath = `files/${fileName}`
|
|
||||||
const arrayBuffer = await readContentOfZipEntry(
|
|
||||||
zip,
|
|
||||||
filePath,
|
|
||||||
'arraybuffer'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!arrayBuffer) {
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const hash = await getHash(arrayBuffer)
|
|
||||||
if (!hash) {
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fileHashes[fileName] = hash
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
const signedEvent = await signEventForMetaFile(
|
const signedEvent = await signEventForMetaFile(
|
||||||
fileHashes,
|
JSON.stringify({
|
||||||
|
fileHashes: currentFileHashes
|
||||||
|
}),
|
||||||
nostrController,
|
nostrController,
|
||||||
setIsLoading
|
setIsLoading
|
||||||
)
|
)
|
||||||
@ -290,8 +362,8 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
const metaCopy = _.cloneDeep(meta)
|
const metaCopy = _.cloneDeep(meta)
|
||||||
|
|
||||||
metaCopy.signedEvents = {
|
metaCopy.docSignatures = {
|
||||||
...metaCopy.signedEvents,
|
...metaCopy.docSignatures,
|
||||||
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
|
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,21 +421,23 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
// check if the current user is the last signer
|
// check if the current user is the last signer
|
||||||
const usersNpub = hexToNpub(usersPubkey!)
|
const usersNpub = hexToNpub(usersPubkey!)
|
||||||
const lastSignerIndex = meta.signers.length - 1
|
const lastSignerIndex = signers.length - 1
|
||||||
const signerIndex = meta.signers.indexOf(usersNpub)
|
const signerIndex = signers.indexOf(usersNpub)
|
||||||
const isLastSigner = signerIndex === lastSignerIndex
|
const isLastSigner = signerIndex === lastSignerIndex
|
||||||
|
|
||||||
// if current user is the last signer, then send DMs to all signers and viewers
|
// if current user is the last signer, then send DMs to all signers and viewers
|
||||||
if (isLastSigner) {
|
if (isLastSigner) {
|
||||||
const userSet = new Set<`npub1${string}`>()
|
const userSet = new Set<`npub1${string}`>()
|
||||||
|
|
||||||
userSet.add(meta.submittedBy)
|
if (submittedBy) {
|
||||||
|
userSet.add(hexToNpub(submittedBy))
|
||||||
|
}
|
||||||
|
|
||||||
meta.signers.forEach((signer) => {
|
signers.forEach((signer) => {
|
||||||
userSet.add(signer)
|
userSet.add(signer)
|
||||||
})
|
})
|
||||||
|
|
||||||
meta.viewers.forEach((viewer) => {
|
viewers.forEach((viewer) => {
|
||||||
userSet.add(viewer)
|
userSet.add(viewer)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -381,7 +455,7 @@ export const SignPage = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const nextSigner = meta.signers[signerIndex + 1]
|
const nextSigner = signers[signerIndex + 1]
|
||||||
await sendDM(
|
await sendDM(
|
||||||
fileUrl,
|
fileUrl,
|
||||||
key,
|
key,
|
||||||
@ -406,28 +480,21 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
const usersNpub = hexToNpub(usersPubkey)
|
const usersNpub = hexToNpub(usersPubkey)
|
||||||
if (
|
if (
|
||||||
!meta.signers.includes(usersNpub) &&
|
!signers.includes(usersNpub) &&
|
||||||
!meta.viewers.includes(usersNpub) &&
|
!viewers.includes(usersNpub) &&
|
||||||
meta.submittedBy !== usersNpub
|
submittedBy !== usersNpub
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
const event: EventTemplate = {
|
const signedEvent = await signEventForMetaFile(
|
||||||
kind: 1,
|
JSON.stringify({
|
||||||
content: '',
|
fileHashes: currentFileHashes
|
||||||
created_at: Math.floor(Date.now() / 1000), // Current timestamp
|
}),
|
||||||
tags: []
|
nostrController,
|
||||||
}
|
setIsLoading
|
||||||
|
)
|
||||||
// Sign the event
|
|
||||||
const signedEvent = await nostrController.signEvent(event).catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
toast.error(err.message || 'Error occurred in signing nostr event')
|
|
||||||
setIsLoading(false) // Set loading state to false
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!signedEvent) return
|
if (!signedEvent) return
|
||||||
|
|
||||||
@ -516,29 +583,33 @@ export const SignPage = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{meta && signedStatus === SignedStatus.Fully_Signed && (
|
{submittedBy && zip && (
|
||||||
<>
|
<>
|
||||||
<DisplayMeta meta={meta} nextSigner={nextSinger} />
|
<DisplayMeta
|
||||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
zip={zip}
|
||||||
<Button onClick={handleExport} variant="contained">
|
submittedBy={submittedBy}
|
||||||
Export
|
signers={signers}
|
||||||
</Button>
|
viewers={viewers}
|
||||||
</Box>
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
{meta && signedStatus === SignedStatus.User_Is_Not_Next_Signer && (
|
{signedStatus === SignedStatus.User_Is_Next_Signer && (
|
||||||
<DisplayMeta meta={meta} nextSigner={nextSinger} />
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||||
)}
|
<Button onClick={handleSign} variant="contained">
|
||||||
|
Sign
|
||||||
{meta && signedStatus === SignedStatus.User_Is_Next_Signer && (
|
</Button>
|
||||||
<>
|
</Box>
|
||||||
<DisplayMeta meta={meta} nextSigner={nextSinger} />
|
)}
|
||||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Button onClick={handleSign} variant="contained">
|
|
||||||
Sign
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@ -547,11 +618,26 @@ export const SignPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DisplayMetaProps = {
|
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
|
nextSigner?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
const DisplayMeta = ({
|
||||||
|
zip,
|
||||||
|
submittedBy,
|
||||||
|
signers,
|
||||||
|
viewers,
|
||||||
|
creatorFileHashes,
|
||||||
|
currentFileHashes,
|
||||||
|
signedBy,
|
||||||
|
nextSigner
|
||||||
|
}: DisplayMetaProps) => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const textColor = theme.palette.getContrastText(
|
const textColor = theme.palette.getContrastText(
|
||||||
@ -564,7 +650,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
|||||||
const [users, setUsers] = useState<User[]>([])
|
const [users, setUsers] = useState<User[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
meta.signers.forEach((signer) => {
|
signers.forEach((signer) => {
|
||||||
const hexKey = npubToHex(signer)
|
const hexKey = npubToHex(signer)
|
||||||
setUsers((prev) => {
|
setUsers((prev) => {
|
||||||
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
||||||
@ -579,7 +665,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
meta.viewers.forEach((viewer) => {
|
viewers.forEach((viewer) => {
|
||||||
const hexKey = npubToHex(viewer)
|
const hexKey = npubToHex(viewer)
|
||||||
setUsers((prev) => {
|
setUsers((prev) => {
|
||||||
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
||||||
@ -593,13 +679,13 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, [meta])
|
}, [signers, viewers])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const metadataController = new MetadataController()
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
const hexKeys: string[] = [
|
const hexKeys: string[] = [
|
||||||
npubToHex(meta.submittedBy)!,
|
npubToHex(submittedBy)!,
|
||||||
...users.map((user) => user.pubkey)
|
...users.map((user) => user.pubkey)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -622,7 +708,19 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [users, meta.submittedBy])
|
}, [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 (
|
return (
|
||||||
<List
|
<List
|
||||||
@ -631,17 +729,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
|||||||
marginTop: 2
|
marginTop: 2
|
||||||
}}
|
}}
|
||||||
subheader={
|
subheader={
|
||||||
<ListSubheader
|
<ListSubheader className={styles.subHeader}>Meta Info</ListSubheader>
|
||||||
sx={{
|
|
||||||
borderBottom: '0.5px solid',
|
|
||||||
paddingBottom: 1,
|
|
||||||
paddingTop: 1,
|
|
||||||
fontSize: '1.5rem'
|
|
||||||
}}
|
|
||||||
className={styles.subHeader}
|
|
||||||
>
|
|
||||||
Meta Info
|
|
||||||
</ListSubheader>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem
|
<ListItem
|
||||||
@ -654,17 +742,14 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
|||||||
Submitted By
|
Submitted By
|
||||||
</Typography>
|
</Typography>
|
||||||
{(function () {
|
{(function () {
|
||||||
const pubkey = npubToHex(meta.submittedBy)
|
const profile = metadata[submittedBy]
|
||||||
const profile = metadata[pubkey!]
|
|
||||||
return (
|
return (
|
||||||
<UserComponent
|
<UserComponent
|
||||||
pubkey={pubkey!}
|
pubkey={submittedBy}
|
||||||
name={
|
name={
|
||||||
profile?.display_name ||
|
profile?.display_name || profile?.name || shorten(submittedBy)
|
||||||
profile?.name ||
|
|
||||||
shorten(meta.submittedBy)
|
|
||||||
}
|
}
|
||||||
image={metadata[meta.submittedBy]?.picture}
|
image={profile?.picture}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
@ -679,13 +764,40 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
|||||||
<Typography variant="h6" sx={{ color: textColor }}>
|
<Typography variant="h6" sx={{ color: textColor }}>
|
||||||
Files
|
Files
|
||||||
</Typography>
|
</Typography>
|
||||||
<ul>
|
<Box className={styles.filesWrapper}>
|
||||||
{Object.keys(meta.fileHashes).map((file, index) => (
|
{Object.entries(currentFileHashes).map(([filename, hash], index) => {
|
||||||
<li key={index} style={{ color: textColor }}>
|
const isValidHash = creatorFileHashes[filename] === hash
|
||||||
{file}
|
|
||||||
</li>
|
return (
|
||||||
))}
|
<Box key={`file-${index}`} className={styles.file}>
|
||||||
</ul>
|
<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>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem sx={{ marginTop: 1 }}>
|
<ListItem sx={{ marginTop: 1 }}>
|
||||||
<Table>
|
<Table>
|
||||||
@ -705,7 +817,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
|||||||
if (user.role === UserRole.signer) {
|
if (user.role === UserRole.signer) {
|
||||||
// check if user has signed the document
|
// check if user has signed the document
|
||||||
const usersNpub = hexToNpub(user.pubkey)
|
const usersNpub = hexToNpub(user.pubkey)
|
||||||
if (usersNpub in meta.signedEvents) {
|
if (signedBy.includes(usersNpub)) {
|
||||||
signedStatus = 'Signed'
|
signedStatus = 'Signed'
|
||||||
}
|
}
|
||||||
// check if user is the next signer
|
// check if user is the next signer
|
||||||
|
@ -10,6 +10,25 @@
|
|||||||
gap: 25px;
|
gap: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subHeader {
|
||||||
|
border-bottom: 0.5px solid;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filesWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-left: 15px;
|
||||||
|
|
||||||
|
.file {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.user {
|
.user {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -15,8 +15,9 @@ import { toast } from 'react-toastify'
|
|||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { UserComponent } from '../../components/username'
|
import { UserComponent } from '../../components/username'
|
||||||
import { MetadataController } from '../../controllers'
|
import { MetadataController } from '../../controllers'
|
||||||
import { Meta, ProfileMetadata } from '../../types'
|
import { CreateSignatureEventContent, Meta, ProfileMetadata } from '../../types'
|
||||||
import {
|
import {
|
||||||
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
parseJson,
|
parseJson,
|
||||||
@ -36,17 +37,64 @@ export const VerifyPage = () => {
|
|||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
|
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
|
const [zip, setZip] = useState<JSZip>()
|
||||||
const [meta, setMeta] = useState<Meta | null>(null)
|
const [meta, setMeta] = useState<Meta | null>(null)
|
||||||
|
|
||||||
|
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 [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (meta) {
|
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'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (arrayBuffer) {
|
||||||
|
const hash = await getHash(arrayBuffer)
|
||||||
|
|
||||||
|
if (hash) {
|
||||||
|
fileHashes[fileName.replace(/^files\//, '')] = hash
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileHashes[fileName.replace(/^files\//, '')] = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentFileHashes(fileHashes)
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCurrentFileHashes()
|
||||||
|
}
|
||||||
|
}, [zip])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submittedBy) {
|
||||||
const metadataController = new MetadataController()
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
const users = [meta.submittedBy, ...meta.signers, ...meta.viewers]
|
const users = [submittedBy, ...signers, ...viewers]
|
||||||
|
|
||||||
users.forEach((user) => {
|
users.forEach((user) => {
|
||||||
const pubkey = npubToHex(user)!
|
const pubkey = npubToHex(user)!
|
||||||
@ -72,7 +120,7 @@ export const VerifyPage = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [meta])
|
}, [submittedBy, signers, viewers])
|
||||||
|
|
||||||
const handleVerify = async () => {
|
const handleVerify = async () => {
|
||||||
if (!selectedFile) return
|
if (!selectedFile) return
|
||||||
@ -85,6 +133,7 @@ export const VerifyPage = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!zip) return
|
if (!zip) return
|
||||||
|
setZip(zip)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Parsing meta.json')
|
setLoadingSpinnerDesc('Parsing meta.json')
|
||||||
|
|
||||||
@ -110,6 +159,51 @@ export const VerifyPage = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
setMeta(parsedMetaJson)
|
setMeta(parsedMetaJson)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
@ -121,7 +215,7 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
if (verifySignature) {
|
if (verifySignature) {
|
||||||
const npub = hexToNpub(pubkey)
|
const npub = hexToNpub(pubkey)
|
||||||
const signedEventString = meta ? meta.signedEvents[npub] : null
|
const signedEventString = meta ? meta.docSignatures[npub] : null
|
||||||
if (signedEventString) {
|
if (signedEventString) {
|
||||||
try {
|
try {
|
||||||
const signedEvent = JSON.parse(signedEventString)
|
const signedEvent = JSON.parse(signedEventString)
|
||||||
@ -150,11 +244,11 @@ export const VerifyPage = () => {
|
|||||||
component="label"
|
component="label"
|
||||||
sx={{
|
sx={{
|
||||||
color: isValidSignature
|
color: isValidSignature
|
||||||
? theme.palette.text.primary
|
? theme.palette.success.light
|
||||||
: theme.palette.error.main
|
: theme.palette.error.main
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
({isValidSignature ? 'Valid' : 'Invalid'} Signature)
|
{isValidSignature ? 'Valid' : 'Invalid'} Signature
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -229,17 +323,19 @@ export const VerifyPage = () => {
|
|||||||
</ListSubheader>
|
</ListSubheader>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem
|
{submittedBy && (
|
||||||
sx={{
|
<ListItem
|
||||||
marginTop: 1,
|
sx={{
|
||||||
gap: '15px'
|
marginTop: 1,
|
||||||
}}
|
gap: '15px'
|
||||||
>
|
}}
|
||||||
<Typography variant="h6" sx={{ color: textColor }}>
|
>
|
||||||
Submitted By
|
<Typography variant="h6" sx={{ color: textColor }}>
|
||||||
</Typography>
|
Submitted By
|
||||||
{displayUser(npubToHex(meta.submittedBy)!)}
|
</Typography>
|
||||||
</ListItem>
|
{displayUser(submittedBy)}
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
sx={{
|
sx={{
|
||||||
@ -253,7 +349,7 @@ export const VerifyPage = () => {
|
|||||||
{displayExportedBy()}
|
{displayExportedBy()}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
{meta.signers.length > 0 && (
|
{signers.length > 0 && (
|
||||||
<ListItem
|
<ListItem
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: 1,
|
marginTop: 1,
|
||||||
@ -265,7 +361,7 @@ export const VerifyPage = () => {
|
|||||||
Signers
|
Signers
|
||||||
</Typography>
|
</Typography>
|
||||||
<ul className={styles.usersList}>
|
<ul className={styles.usersList}>
|
||||||
{meta.signers.map((signer) => (
|
{signers.map((signer) => (
|
||||||
<li
|
<li
|
||||||
key={signer}
|
key={signer}
|
||||||
style={{
|
style={{
|
||||||
@ -282,7 +378,7 @@ export const VerifyPage = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{meta.viewers.length > 0 && (
|
{viewers.length > 0 && (
|
||||||
<ListItem
|
<ListItem
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: 1,
|
marginTop: 1,
|
||||||
@ -294,7 +390,7 @@ export const VerifyPage = () => {
|
|||||||
Viewers
|
Viewers
|
||||||
</Typography>
|
</Typography>
|
||||||
<ul className={styles.usersList}>
|
<ul className={styles.usersList}>
|
||||||
{meta.viewers.map((viewer) => (
|
{viewers.map((viewer) => (
|
||||||
<li key={viewer} style={{ color: textColor }}>
|
<li key={viewer} style={{ color: textColor }}>
|
||||||
{displayUser(npubToHex(viewer)!)}
|
{displayUser(npubToHex(viewer)!)}
|
||||||
</li>
|
</li>
|
||||||
@ -313,13 +409,37 @@ export const VerifyPage = () => {
|
|||||||
<Typography variant="h6" sx={{ color: textColor }}>
|
<Typography variant="h6" sx={{ color: textColor }}>
|
||||||
Files
|
Files
|
||||||
</Typography>
|
</Typography>
|
||||||
<ul>
|
<Box className={styles.filesWrapper}>
|
||||||
{Object.keys(meta.fileHashes).map((file, index) => (
|
{Object.entries(currentFileHashes).map(
|
||||||
<li key={index} style={{ color: textColor }}>
|
([filename, hash], index) => {
|
||||||
{file}
|
const isValidHash = creatorFileHashes[filename] === hash
|
||||||
</li>
|
|
||||||
))}
|
return (
|
||||||
</ul>
|
<Box key={`file-${index}`} className={styles.file}>
|
||||||
|
<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>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
</>
|
</>
|
||||||
|
@ -10,6 +10,19 @@
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filesWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-left: 15px;
|
||||||
|
|
||||||
|
.file {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.usersList {
|
.usersList {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -9,10 +9,13 @@ export interface User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Meta {
|
export interface Meta {
|
||||||
|
createSignature: string
|
||||||
|
docSignatures: { [key: `npub1${string}`]: string }
|
||||||
|
exportSignature?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSignatureEventContent {
|
||||||
signers: `npub1${string}`[]
|
signers: `npub1${string}`[]
|
||||||
viewers: `npub1${string}`[]
|
viewers: `npub1${string}`[]
|
||||||
fileHashes: { [key: string]: string }
|
fileHashes: { [key: string]: string }
|
||||||
submittedBy: `npub1${string}`
|
|
||||||
signedEvents: { [key: `npub1${string}`]: string }
|
|
||||||
exportSignature?: string
|
|
||||||
}
|
}
|
||||||
|
@ -177,22 +177,20 @@ export const sendDM = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Signs an event for a meta.json file.
|
* Signs an event for a meta.json file.
|
||||||
* @param fileHashes Object containing file hashes.
|
* @param content contains content for event.
|
||||||
* @param nostrController The NostrController instance for signing the event.
|
* @param nostrController The NostrController instance for signing the event.
|
||||||
* @param setIsLoading Function to set loading state in the component.
|
* @param setIsLoading Function to set loading state in the component.
|
||||||
* @returns A Promise resolving to the signed event, or null if signing fails.
|
* @returns A Promise resolving to the signed event, or null if signing fails.
|
||||||
*/
|
*/
|
||||||
export const signEventForMetaFile = async (
|
export const signEventForMetaFile = async (
|
||||||
fileHashes: {
|
content: string,
|
||||||
[key: string]: string
|
|
||||||
},
|
|
||||||
nostrController: NostrController,
|
nostrController: NostrController,
|
||||||
setIsLoading: (value: React.SetStateAction<boolean>) => void
|
setIsLoading: (value: React.SetStateAction<boolean>) => void
|
||||||
) => {
|
) => {
|
||||||
// Construct the event metadata for the meta file
|
// Construct the event metadata for the meta file
|
||||||
const event: EventTemplate = {
|
const event: EventTemplate = {
|
||||||
kind: 1, // Event type for meta file
|
kind: 1, // Event type for meta file
|
||||||
content: JSON.stringify(fileHashes), // Convert file hashes to JSON string
|
content: content, // content for event
|
||||||
created_at: Math.floor(Date.now() / 1000), // Current timestamp
|
created_at: Math.floor(Date.now() / 1000), // Current timestamp
|
||||||
tags: []
|
tags: []
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user