diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts
index 613ec44..88f8a75 100644
--- a/src/controllers/MetadataController.ts
+++ b/src/controllers/MetadataController.ts
@@ -46,7 +46,6 @@ export class MetadataController extends EventEmitter {
const pool = new SimplePool()
- // todo: use nostrController to get event
// Try to get the metadata event from a special relay (wss://purplepag.es)
const metadataEvent = await pool
.get([this.specialMetadataRelay], eventFilter)
@@ -62,14 +61,17 @@ export class MetadataController extends EventEmitter {
verifyEvent(metadataEvent) // Verify the event's authenticity
) {
// If there's no current event or the new metadata event is more recent
- if (!currentEvent || metadataEvent.created_at > currentEvent.created_at) {
+ if (
+ !currentEvent ||
+ metadataEvent.created_at >= currentEvent.created_at
+ ) {
// Handle the new metadata event
this.handleNewMetadataEvent(metadataEvent)
- return metadataEvent
}
+
+ return metadataEvent
}
- // todo use nostr controller to find event from connected relays
// If no valid metadata event is found from the special relay, get the most popular relays
const mostPopularRelays = await this.nostrController.getMostPopularRelays()
@@ -125,11 +127,11 @@ export class MetadataController extends EventEmitter {
// If cached metadata is found, check its validity
if (cachedMetadataEvent) {
- const oneDayInMS = 24 * 60 * 60 * 1000 // Number of milliseconds in one day
+ const oneWeekInMS = 7 * 24 * 60 * 60 * 1000 // Number of milliseconds in one week
- // Check if the cached metadata is older than one day
- if (Date.now() - cachedMetadataEvent.cachedAt > oneDayInMS) {
- // If older than one day, find the metadata from relays in background
+ // Check if the cached metadata is older than one week
+ if (Date.now() - cachedMetadataEvent.cachedAt > oneWeekInMS) {
+ // If older than one week, find the metadata from relays in background
this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event)
}
diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx
index 2c13cf2..274c849 100644
--- a/src/layouts/Main.tsx
+++ b/src/layouts/Main.tsx
@@ -1,30 +1,40 @@
import { Box } from '@mui/material'
import Container from '@mui/material/Container'
+import { Event, kinds } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Outlet } from 'react-router-dom'
import { AppBar } from '../components/AppBar/AppBar'
-import { restoreState, setAuthState, setMetadataEvent } from '../store/actions'
+import { LoadingSpinner } from '../components/LoadingSpinner'
+import { MetadataController, NostrController } from '../controllers'
+import {
+ restoreState,
+ setAuthState,
+ setMetadataEvent,
+ updateUserAppData
+} from '../store/actions'
+import { LoginMethods } from '../store/auth/types'
+import { State } from '../store/rootReducer'
+import { Dispatch } from '../store/store'
+import { setUserRobotImage } from '../store/userRobotImage/action'
import {
clearAuthToken,
clearState,
getRoboHashPicture,
+ getUsersAppData,
loadState,
saveNsecBunkerDelegatedKey,
subscribeForSigits
} from '../utils'
-import { LoadingSpinner } from '../components/LoadingSpinner'
-import { Dispatch } from '../store/store'
-import { MetadataController, NostrController } from '../controllers'
-import { LoginMethods } from '../store/auth/types'
-import { setUserRobotImage } from '../store/userRobotImage/action'
-import { State } from '../store/rootReducer'
-import { Event, kinds } from 'nostr-tools'
+import { useAppSelector } from '../hooks'
+import { SubCloser } from 'nostr-tools/abstract-pool'
export const MainLayout = () => {
const dispatch: Dispatch = useDispatch()
const [isLoading, setIsLoading] = useState(true)
+ const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState(`Loading App`)
const authState = useSelector((state: State) => state.auth)
+ const usersAppData = useAppSelector((state) => state.userAppData)
useEffect(() => {
const metadataController = new MetadataController()
@@ -84,11 +94,47 @@ export const MainLayout = () => {
metadataController.findMetadata(usersPubkey).then((metadataEvent) => {
if (metadataEvent) handleMetadataEvent(metadataEvent)
})
+
+ setLoadingSpinnerDesc(`Fetching user's app data`)
+ getUsersAppData()
+ .then((appData) => {
+ if (appData) {
+ dispatch(updateUserAppData(appData))
+ }
+ })
+ .finally(() => setIsLoading(false))
+ } else {
+ setIsLoading(false)
+ }
+ } else {
+ setIsLoading(false)
+ }
+ }, [dispatch])
+
+ useEffect(() => {
+ let subCloser: SubCloser | null = null
+
+ if (authState.loggedIn && usersAppData) {
+ const pubkey = authState.usersPubkey || authState.keyPair?.public
+
+ if (pubkey) {
+ /**
+ * Sigit notifications are wrapped using nip 59 seal and gift wrap scheme.
+ * According to nip59 created_at for seal and gift wrap should be tweaked to thwart time-analysis attacks.
+ * For the above purpose created_at is set to random time upto 2 days in past
+ */
+ subscribeForSigits(pubkey).then((res) => {
+ subCloser = res || null
+ })
}
}
- setIsLoading(false)
- }, [dispatch])
+ return () => {
+ if (subCloser) {
+ subCloser.close()
+ }
+ }
+ }, [authState, usersAppData])
/**
* When authState change user logged in / or app reloaded
@@ -101,13 +147,11 @@ export const MainLayout = () => {
if (pubkey) {
dispatch(setUserRobotImage(getRoboHashPicture(pubkey)))
-
- subscribeForSigits(pubkey)
}
}
}, [authState])
- if (isLoading) return
+ if (isLoading) return
return (
<>
diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx
index 962afd6..7c49dd4 100644
--- a/src/pages/create/index.tsx
+++ b/src/pages/create/index.tsx
@@ -23,19 +23,23 @@ import saveAs from 'file-saver'
import JSZip from 'jszip'
import { MuiFileInput } from 'mui-file-input'
import { Event, kinds } from 'nostr-tools'
-import { useEffect, useMemo, useRef, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { DndProvider, useDrag, useDrop } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { useSelector } from 'react-redux'
import { useLocation, useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
-import { v4 as uuidV4 } from 'uuid'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { UserComponent } from '../../components/username'
import { MetadataController, NostrController } from '../../controllers'
-import { appPrivateRoutes } from '../../routes'
import { State } from '../../store/rootReducer'
-import { Meta, ProfileMetadata, User, UserRole } from '../../types'
+import {
+ CreateSignatureEventContent,
+ Meta,
+ ProfileMetadata,
+ User,
+ UserRole
+} from '../../types'
import {
encryptArrayBuffer,
generateEncryptionKey,
@@ -54,6 +58,7 @@ import {
uploadToFileStorage
} from '../../utils'
import styles from './style.module.scss'
+import { appPrivateRoutes } from '../../routes'
export const CreatePage = () => {
const navigate = useNavigate()
@@ -83,10 +88,6 @@ export const CreatePage = () => {
setAuthUrl(url)
})
- const uuid = useMemo(() => {
- return uuidV4()
- }, [])
-
useEffect(() => {
if (uploadedFile) {
setSelectedFiles([uploadedFile])
@@ -295,56 +296,6 @@ export const CreatePage = () => {
return fileHashes
}
- // initialize a zip file with the selected files and generate creator's signature
- const initZipFileAndCreatorSignature = async (
- encryptionKey: string,
- fileHashes: {
- [key: string]: string
- }
- ): Promise<{ zip: JSZip; createSignature: string } | null> => {
- const zip = new JSZip()
-
- selectedFiles.forEach((file) => {
- zip.file(`files/${file.name}`, file)
- })
-
- // generate key pairs for decryption
- const pubkeys = users.map((user) => user.pubkey)
- // also add creator in the list
- if (pubkeys.includes(usersPubkey!)) {
- pubkeys.push(usersPubkey!)
- }
-
- const keys = await generateKeys(pubkeys, encryptionKey)
-
- const signers = users.filter((user) => user.role === UserRole.signer)
- const viewers = users.filter((user) => user.role === UserRole.viewer)
-
- setLoadingSpinnerDesc('Signing nostr event')
-
- const createSignature = await signEventForMetaFile(
- JSON.stringify({
- signers: signers.map((signer) => hexToNpub(signer.pubkey)),
- viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
- fileHashes,
- keys
- }),
- nostrController,
- setIsLoading
- )
-
- if (!createSignature) return null
-
- try {
- return {
- zip,
- createSignature: JSON.stringify(createSignature, null, 2)
- }
- } catch (error) {
- return null
- }
- }
-
// Handle errors during zip file generation
const handleZipError = (err: any) => {
console.log('Error in zip:>> ', err)
@@ -377,7 +328,7 @@ export const CreatePage = () => {
return encryptArrayBuffer(arraybuffer, encryptionKey)
}
- // create final zip file
+ // create final zip file for offline mode
const createFinalZipFile = async (
encryptedArrayBuffer: ArrayBuffer,
encryptionKey: string
@@ -423,28 +374,6 @@ export const CreatePage = () => {
return finalZipFile
}
- const handleOnlineFlow = async (
- encryptedArrayBuffer: ArrayBuffer,
- meta: Meta
- ) => {
- const unixNow = now()
- const blob = new Blob([encryptedArrayBuffer])
- // Create a File object with the Blob data
- const file = new File([blob], `compressed-${unixNow}.sigit`, {
- type: 'application/sigit'
- })
-
- const fileUrl = await uploadFile(file)
- if (!fileUrl) return
-
- const updatedEvent = await updateUsersAppData(fileUrl, meta)
- if (!updatedEvent) return
-
- await sendDMs(fileUrl, meta)
-
- navigate(appPrivateRoutes.sign, { state: { sigit: { fileUrl, meta } } })
- }
-
// Handle errors during file upload
const handleUploadError = (err: any) => {
console.log('Error in upload:>> ', err)
@@ -454,13 +383,19 @@ export const CreatePage = () => {
}
// Upload the file to the storage
- const uploadFile = async (file: File): Promise => {
- setIsLoading(true)
- setLoadingSpinnerDesc('Uploading sigit to file storage.')
+ const uploadFile = async (
+ arrayBuffer: ArrayBuffer
+ ): Promise => {
+ const unixNow = now()
+ const blob = new Blob([arrayBuffer])
+ // Create a File object with the Blob data
+ const file = new File([blob], `compressed-${unixNow}.sigit`, {
+ type: 'application/sigit'
+ })
const fileUrl = await uploadToFileStorage(file, nostrController)
.then((url) => {
- toast.success('Sigit uploaded to file storage')
+ toast.success('files.zip uploaded to file storage')
return url
})
.catch(handleUploadError)
@@ -468,24 +403,6 @@ export const CreatePage = () => {
return fileUrl
}
- // Send DMs to signers and viewers with the file URL
- const sendDMs = async (fileUrl: string, meta: Meta) => {
- setLoadingSpinnerDesc('Sending DM to signers/viewers')
-
- const signers = users.filter((user) => user.role === UserRole.signer)
- const viewers = users.filter((user) => user.role === UserRole.viewer)
-
- const receivers =
- signers.length > 0
- ? [signers[0].pubkey]
- : viewers.map((viewer) => viewer.pubkey)
-
- const promises = receivers.map((receiver) =>
- sendNotification(receiver, meta, fileUrl)
- )
- await Promise.allSettled(promises)
- }
-
// Manage offline scenarios for signing or viewing the file
const handleOfflineFlow = async (
encryptedArrayBuffer: ArrayBuffer,
@@ -501,49 +418,195 @@ export const CreatePage = () => {
saveAs(finalZipFile, 'request.sigit.zip')
}
+ const generateFilesZip = async (): Promise => {
+ const zip = new JSZip()
+ selectedFiles.forEach((file) => {
+ zip.file(file.name, file)
+ })
+
+ const arraybuffer = await zip
+ .generateAsync({
+ type: 'arraybuffer',
+ compression: 'DEFLATE',
+ compressionOptions: { level: 6 }
+ })
+ .catch(handleZipError)
+
+ return arraybuffer
+ }
+
+ const generateCreateSignature = async (
+ fileHashes: {
+ [key: string]: string
+ },
+ zipUrl: string
+ ) => {
+ const signers = users.filter((user) => user.role === UserRole.signer)
+ const viewers = users.filter((user) => user.role === UserRole.viewer)
+
+ const content: CreateSignatureEventContent = {
+ signers: signers.map((signer) => hexToNpub(signer.pubkey)),
+ viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
+ fileHashes,
+ zipUrl,
+ title
+ }
+
+ setLoadingSpinnerDesc('Signing nostr event for create signature')
+
+ try {
+ const createSignature = await signEventForMetaFile(
+ JSON.stringify(content),
+ nostrController,
+ setIsLoading
+ )
+
+ if (!createSignature) return null
+
+ return JSON.stringify(createSignature, null, 2)
+ } catch (error) {
+ return null
+ }
+ }
+
+ // Send notifications to signers and viewers
+ const sendNotifications = (meta: Meta) => {
+ const signers = users.filter((user) => user.role === UserRole.signer)
+ const viewers = users.filter((user) => user.role === UserRole.viewer)
+
+ // no need to send notification to self so remove it from the list
+ const receivers = (
+ signers.length > 0
+ ? [signers[0].pubkey]
+ : viewers.map((viewer) => viewer.pubkey)
+ ).filter((receiver) => receiver !== usersPubkey)
+
+ const promises = receivers.map((receiver) =>
+ sendNotification(receiver, meta)
+ )
+
+ return promises
+ }
+
const handleCreate = async () => {
if (!validateInputs()) return
setIsLoading(true)
- setLoadingSpinnerDesc('Generating hashes for files')
-
+ setLoadingSpinnerDesc('Generating file hashes')
const fileHashes = await generateFileHashes()
- if (!fileHashes) return
-
- const encryptionKey = await generateEncryptionKey()
-
- const createZipResponse = await initZipFileAndCreatorSignature(
- encryptionKey,
- fileHashes
- )
- if (!createZipResponse) return
-
- const { zip, createSignature } = createZipResponse
-
- // create content for meta file
- const meta: Meta = {
- uuid,
- title,
- modifiedAt: now(),
- createSignature,
- docSignatures: {}
+ if (!fileHashes) {
+ setIsLoading(false)
+ return
}
- setLoadingSpinnerDesc('Generating zip file')
-
- const arrayBuffer = await generateZipFile(zip)
- if (!arrayBuffer) return
-
- setLoadingSpinnerDesc('Encrypting zip file')
- const encryptedArrayBuffer = await encryptZipFile(
- arrayBuffer,
- encryptionKey
- )
+ setLoadingSpinnerDesc('Generating encryption key')
+ const encryptionKey = await generateEncryptionKey()
if (await isOnline()) {
- await handleOnlineFlow(encryptedArrayBuffer, meta)
+ setLoadingSpinnerDesc('generating files.zip')
+ const arrayBuffer = await generateFilesZip()
+ if (!arrayBuffer) {
+ setIsLoading(false)
+ return
+ }
+
+ setLoadingSpinnerDesc('Encrypting files.zip')
+ const encryptedArrayBuffer = await encryptZipFile(
+ arrayBuffer,
+ encryptionKey
+ )
+
+ setLoadingSpinnerDesc('Uploading files.zip to file storage')
+ const fileUrl = await uploadFile(encryptedArrayBuffer)
+ if (!fileUrl) {
+ setIsLoading(false)
+ return
+ }
+
+ setLoadingSpinnerDesc('Generating create signature')
+ const createSignature = await generateCreateSignature(fileHashes, fileUrl)
+ if (!createSignature) {
+ setIsLoading(false)
+ return
+ }
+
+ setLoadingSpinnerDesc('Generating keys for decryption')
+
+ // generate key pairs for decryption
+ const pubkeys = users.map((user) => user.pubkey)
+ // also add creator in the list
+ if (pubkeys.includes(usersPubkey!)) {
+ pubkeys.push(usersPubkey!)
+ }
+
+ const keys = await generateKeys(pubkeys, encryptionKey)
+
+ if (!keys) {
+ setIsLoading(false)
+ return
+ }
+ const meta: Meta = {
+ createSignature,
+ keys,
+ modifiedAt: now(),
+ docSignatures: {}
+ }
+
+ setLoadingSpinnerDesc('Updating user app data')
+ const event = await updateUsersAppData(meta)
+ if (!event) {
+ setIsLoading(false)
+ return
+ }
+
+ setLoadingSpinnerDesc('Sending notifications to counterparties')
+ const promises = sendNotifications(meta)
+
+ await Promise.all(promises)
+ .then(() => {
+ toast.success('Notifications sent successfully')
+ })
+ .catch(() => {
+ toast.error('Failed to publish notifications')
+ })
+
+ navigate(appPrivateRoutes.sign, { state: { meta: meta } })
} else {
- // todo: fix offline flow
+ const zip = new JSZip()
+
+ selectedFiles.forEach((file) => {
+ zip.file(`files/${file.name}`, file)
+ })
+
+ setLoadingSpinnerDesc('Generating create signature')
+ const createSignature = await generateCreateSignature(fileHashes, '')
+ if (!createSignature) return
+
+ const meta: Meta = {
+ createSignature,
+ modifiedAt: now(),
+ docSignatures: {}
+ }
+
+ // add meta to zip
+ try {
+ const stringifiedMeta = JSON.stringify(meta, null, 2)
+ zip.file('meta.json', stringifiedMeta)
+ } catch (err) {
+ console.error(err)
+ toast.error('An error occurred in converting meta json to string')
+ return null
+ }
+
+ const arrayBuffer = await generateZipFile(zip)
+ if (!arrayBuffer) return
+
+ setLoadingSpinnerDesc('Encrypting zip file')
+ const encryptedArrayBuffer = await encryptZipFile(
+ arrayBuffer,
+ encryptionKey
+ )
+
await handleOfflineFlow(encryptedArrayBuffer, encryptionKey)
}
}
diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx
index 30d39db..0f5c775 100644
--- a/src/pages/home/index.tsx
+++ b/src/pages/home/index.tsx
@@ -1,59 +1,38 @@
import { Add, CalendarMonth, Description, Upload } from '@mui/icons-material'
import { Box, Button, Tooltip, Typography } from '@mui/material'
import JSZip from 'jszip'
+import { Event, kinds, verifyEvent } from 'nostr-tools'
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
+import { UserComponent } from '../../components/username'
+import { MetadataController } from '../../controllers'
+import { useAppSelector } from '../../hooks'
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
-import styles from './style.module.scss'
-import { MetadataController, NostrController } from '../../controllers'
+import { CreateSignatureEventContent, Meta, ProfileMetadata } from '../../types'
import {
formatTimestamp,
- getUsersAppData,
hexToNpub,
npubToHex,
parseJson,
shorten
} from '../../utils'
-import { LoadingSpinner } from '../../components/LoadingSpinner'
-import {
- CreateSignatureEventContent,
- Meta,
- ProfileMetadata,
- Sigit
-} from '../../types'
-import { Event, kinds, verifyEvent } from 'nostr-tools'
-import { UserComponent } from '../../components/username'
+import styles from './style.module.scss'
export const HomePage = () => {
const navigate = useNavigate()
const fileInputRef = useRef(null)
- const [isLoading, setIsLoading] = useState(true)
- const [loadingSpinnerDesc] = useState(`Finding user's app data`)
- const [authUrl, setAuthUrl] = useState()
- const [sigits, setSigits] = useState([])
-
+ const [sigits, setSigits] = useState([])
const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>(
{}
)
+ const usersAppData = useAppSelector((state) => state.userAppData)
useEffect(() => {
- const nostrController = NostrController.getInstance()
- // Set up event listener for authentication event
- nostrController.on('nsecbunker-auth', (url) => {
- setAuthUrl(url)
- })
-
- getUsersAppData()
- .then((res) => {
- if (res) {
- setSigits(Object.values(res))
- }
- })
- .finally(() => {
- setIsLoading(false)
- })
- }, [])
+ if (usersAppData) {
+ setSigits(Object.values(usersAppData.sigits))
+ }
+ }, [usersAppData])
const handleUploadClick = () => {
if (fileInputRef.current) {
@@ -101,20 +80,8 @@ export const HomePage = () => {
}
}
- if (authUrl) {
- return (
-
- )
- }
-
return (
<>
- {isLoading && }
@@ -180,10 +147,10 @@ export const HomePage = () => {
- {sigits.map((sigit) => (
+ {sigits.map((sigit, index) => (
@@ -195,22 +162,31 @@ export const HomePage = () => {
}
type SigitProps = {
- sigit: Sigit
+ meta: Meta
profiles: { [key: string]: ProfileMetadata }
setProfiles: Dispatch>
}
-const DisplaySigit = ({ sigit, profiles, setProfiles }: SigitProps) => {
+enum SignedStatus {
+ Partial = 'Partially Signed',
+ Complete = 'Completely Signed'
+}
+
+const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => {
const navigate = useNavigate()
+ const [title, setTitle] = useState()
const [createdAt, setCreatedAt] = useState('')
const [submittedBy, setSubmittedBy] = useState()
const [signers, setSigners] = useState<`npub1${string}`[]>([])
+ const [signedStatus, setSignedStatus] = useState(
+ SignedStatus.Partial
+ )
useEffect(() => {
const extractInfo = async () => {
const createSignatureEvent = await parseJson(
- sigit.meta.createSignature
+ meta.createSignature
).catch((err) => {
console.log('err in parsing the createSignature event:>> ', err)
toast.error(
@@ -238,11 +214,20 @@ const DisplaySigit = ({ sigit, profiles, setProfiles }: SigitProps) => {
if (!createSignatureContent) return
+ setTitle(createSignatureContent.title)
setSubmittedBy(createSignatureEvent.pubkey)
setSigners(createSignatureContent.signers)
+
+ const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
+ const isCompletelySigned = createSignatureContent.signers.every(
+ (signer) => signedBy.includes(signer)
+ )
+ if (isCompletelySigned) {
+ setSignedStatus(SignedStatus.Complete)
+ }
}
extractInfo()
- }, [sigit])
+ }, [meta])
useEffect(() => {
const hexKeys: string[] = []
@@ -285,7 +270,11 @@ const DisplaySigit = ({ sigit, profiles, setProfiles }: SigitProps) => {
}, [submittedBy, signers])
const handleNavigation = () => {
- navigate(appPrivateRoutes.sign, { state: { sigit } })
+ if (signedStatus === SignedStatus.Complete) {
+ navigate(appPublicRoutes.verify, { state: { meta } })
+ } else {
+ navigate(appPrivateRoutes.sign, { state: { meta } })
+ }
}
return (
@@ -314,7 +303,7 @@ const DisplaySigit = ({ sigit, profiles, setProfiles }: SigitProps) => {
>
- {sigit.meta.title}
+ {title}
{submittedBy &&
(function () {
@@ -344,7 +333,7 @@ const DisplaySigit = ({ sigit, profiles, setProfiles }: SigitProps) => {
return (
diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx
index ae30012..fa5410b 100644
--- a/src/pages/sign/index.tsx
+++ b/src/pages/sign/index.tsx
@@ -13,16 +13,11 @@ import { LoadingSpinner } from '../../components/LoadingSpinner'
import { NostrController } from '../../controllers'
import { appPublicRoutes } from '../../routes'
import { State } from '../../store/rootReducer'
-import {
- CreateSignatureEventContent,
- Meta,
- Sigit,
- SignedEvent
-} from '../../types'
+import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
import {
decryptArrayBuffer,
encryptArrayBuffer,
- extractEncryptionKey,
+ extractZipUrlAndEncryptionKey,
generateEncryptionKey,
generateKeysFile,
getHash,
@@ -48,7 +43,7 @@ export const SignPage = () => {
const navigate = useNavigate()
const location = useLocation()
const {
- sigit,
+ meta: metaInNavState,
arrayBuffer: decryptedArrayBuffer,
uploadedZip
} = location.state || {}
@@ -57,12 +52,11 @@ export const SignPage = () => {
const [selectedFile, setSelectedFile] = useState(null)
- const [zip, setZip] = useState()
+ const [files, setFiles] = useState<{ [filename: string]: ArrayBuffer }>({})
const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
- const [fileUrl, setFileUrl] = useState()
const [meta, setMeta] = useState(null)
const [signedStatus, setSignedStatus] = useState()
@@ -89,41 +83,6 @@ export const SignPage = () => {
const [authUrl, setAuthUrl] = useState()
const nostrController = NostrController.getInstance()
- useEffect(() => {
- 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 (signers.length > 0) {
// check if all signers have signed then its fully signed
@@ -223,65 +182,32 @@ export const SignPage = () => {
}, [meta])
useEffect(() => {
- if (sigit) {
+ if (metaInNavState) {
const processSigit = async () => {
setIsLoading(true)
- setLoadingSpinnerDesc(
- 'Extracting encryption key from creator signature'
- )
+ setLoadingSpinnerDesc('Extracting zipUrl and encryption key from meta')
- const { fileUrl, meta } = sigit as Sigit
-
- const encryptionKey = await extractEncryptionKey(
- meta.createSignature
- ).then(async (res) => {
- if (!res) return null
-
- const { sender, encryptedKey } = res
- const decrypted = await nostrController
- .nip04Decrypt(sender, encryptedKey)
- .catch((err) => {
- console.log('An error occurred in decrypting encryption key', err)
- toast.error('An error occurred in decrypting encryption key')
- return null
- })
-
- return decrypted
- })
-
- if (!encryptionKey) {
+ const res = await extractZipUrlAndEncryptionKey(metaInNavState)
+ if (!res) {
setIsLoading(false)
return
}
+ const { zipUrl, encryptionKey } = res
+
setLoadingSpinnerDesc('Fetching file from file server')
- setFileUrl(fileUrl)
axios
- .get(fileUrl, {
+ .get(zipUrl, {
responseType: 'arraybuffer'
})
- .then(async (res) => {
- const fileName = fileUrl.split('/').pop()
- const file = new File([res.data], fileName!)
-
- const encryptedArrayBuffer = await file.arrayBuffer()
- const arrayBuffer = await decryptArrayBuffer(
- encryptedArrayBuffer,
- encryptionKey
- ).catch((err) => {
- console.log('err in decryption:>> ', err)
- toast.error(
- err.message || 'An error occurred in decrypting file.'
- )
- return null
- })
-
- if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer, meta)
+ .then((res) => {
+ handleArrayBufferFromBlossom(res.data, encryptionKey)
+ setMeta(metaInNavState)
})
.catch((err) => {
- console.error(`error occurred in getting file from ${fileUrl}`, err)
+ console.error(`error occurred in getting file from ${zipUrl}`, err)
toast.error(
- err.message || `error occurred in getting file from ${fileUrl}`
+ err.message || `error occurred in getting file from ${zipUrl}`
)
})
.finally(() => {
@@ -310,7 +236,63 @@ export const SignPage = () => {
setIsLoading(false)
setDisplayInput(true)
}
- }, [decryptedArrayBuffer, uploadedZip])
+ }, [decryptedArrayBuffer, uploadedZip, metaInNavState])
+
+ const handleArrayBufferFromBlossom = async (
+ arrayBuffer: ArrayBuffer,
+ encryptionKey: string
+ ) => {
+ // array buffer returned from blossom is encrypted.
+ // So, first decrypt it
+ const decrypted = await decryptArrayBuffer(
+ arrayBuffer,
+ encryptionKey
+ ).catch((err) => {
+ console.log('err in decryption:>> ', err)
+ toast.error(err.message || 'An error occurred in decrypting file.')
+ setIsLoading(false)
+ return null
+ })
+
+ if (!decrypted) return
+
+ const zip = await JSZip.loadAsync(decrypted).catch((err) => {
+ console.log('err in loading zip file :>> ', err)
+ toast.error(err.message || 'An error occurred in loading zip file.')
+ setIsLoading(false)
+ return null
+ })
+
+ if (!zip) return
+
+ const files: { [filename: string]: ArrayBuffer } = {}
+ const fileHashes: { [key: string]: string | null } = {}
+ const fileNames = Object.values(zip.files).map((entry) => entry.name)
+
+ // generate hashes for all files in 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) {
+ files[fileName] = arrayBuffer
+
+ const hash = await getHash(arrayBuffer)
+ if (hash) {
+ fileHashes[fileName] = hash
+ }
+ } else {
+ fileHashes[fileName] = null
+ }
+ }
+
+ setFiles(files)
+ setCurrentFileHashes(fileHashes)
+ }
const parseKeysJson = async (zip: JSZip) => {
const keysFileContent = await readContentOfZipEntry(
@@ -405,10 +387,7 @@ export const SignPage = () => {
return null
}
- const handleDecryptedArrayBuffer = async (
- arrayBuffer: ArrayBuffer,
- meta?: Meta
- ) => {
+ const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => {
const decryptedZipFile = new File([arrayBuffer], 'decrypted.zip')
setLoadingSpinnerDesc('Parsing zip file')
@@ -421,36 +400,62 @@ export const SignPage = () => {
if (!zip) return
- setZip(zip)
- setDisplayInput(false)
+ const files: { [filename: string]: ArrayBuffer } = {}
+ const fileHashes: { [key: string]: string | null } = {}
+ const fileNames = Object.values(zip.files)
+ .filter((entry) => entry.name.startsWith('files/') && !entry.dir)
+ .map((entry) => entry.name)
- let parsedMetaJson: Meta | null = null
-
- if (meta) {
- parsedMetaJson = meta
- } else {
- setLoadingSpinnerDesc('Parsing meta.json')
-
- const metaFileContent = await readContentOfZipEntry(
+ // generate hashes for all entries in files folder of zipArchive
+ // these hashes can be used to verify the originality of files
+ for (let fileName of fileNames) {
+ const arrayBuffer = await readContentOfZipEntry(
zip,
- 'meta.json',
- 'string'
+ fileName,
+ 'arraybuffer'
)
- if (!metaFileContent) {
- setIsLoading(false)
- return
- }
+ fileName = fileName.replace(/^files\//, '')
+ if (arrayBuffer) {
+ files[fileName] = arrayBuffer
- parsedMetaJson = await parseJson(metaFileContent).catch((err) => {
+ const hash = await getHash(arrayBuffer)
+ if (hash) {
+ fileHashes[fileName] = hash
+ }
+ } else {
+ fileHashes[fileName] = null
+ }
+ }
+
+ setFiles(files)
+ setCurrentFileHashes(fileHashes)
+
+ setDisplayInput(false)
+
+ setLoadingSpinnerDesc('Parsing meta.json')
+
+ const metaFileContent = await readContentOfZipEntry(
+ zip,
+ 'meta.json',
+ 'string'
+ )
+
+ if (!metaFileContent) {
+ setIsLoading(false)
+ return
+ }
+
+ const parsedMetaJson = await parseJson(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
- })
- }
+ }
+ )
setMeta(parsedMetaJson)
}
@@ -467,7 +472,7 @@ export const SignPage = () => {
}
const handleSign = async () => {
- if (!zip || !meta) return
+ if (Object.entries(files).length === 0 || !meta) return
setIsLoading(true)
@@ -480,13 +485,12 @@ export const SignPage = () => {
if (!signedEvent) return
const updatedMeta = updateMetaSignatures(meta, signedEvent)
- setMeta(updatedMeta)
if (await isOnline()) {
- if (fileUrl) await handleOnlineFlow(fileUrl, updatedMeta)
+ await handleOnlineFlow(updatedMeta)
} else {
- // todo: fix offline flow
- // handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false))
+ setMeta(updatedMeta)
+ setIsLoading(false)
}
}
@@ -585,31 +589,33 @@ export const SignPage = () => {
return null
}
- // Handle the online flow: upload file and send DMs
- const handleOnlineFlow = async (fileUrl: string, meta: Meta) => {
+ // Handle the online flow: update users app data and send notifications
+ const handleOnlineFlow = async (meta: Meta) => {
setLoadingSpinnerDesc('Updating users app data')
- const updatedEvent = await updateUsersAppData(fileUrl, meta)
+ const updatedEvent = await updateUsersAppData(meta)
if (!updatedEvent) {
setIsLoading(false)
return
}
const userSet = new Set<`npub1${string}`>()
- if (submittedBy) {
+ if (submittedBy && submittedBy !== usersPubkey) {
userSet.add(hexToNpub(submittedBy))
}
+ const usersNpub = hexToNpub(usersPubkey!)
const isLastSigner = checkIsLastSigner(signers)
if (isLastSigner) {
signers.forEach((signer) => {
- userSet.add(signer)
+ if (signer !== usersNpub) {
+ userSet.add(signer)
+ }
})
viewers.forEach((viewer) => {
userSet.add(viewer)
})
} else {
- const usersNpub = hexToNpub(usersPubkey!)
const currentSignerIndex = signers.indexOf(usersNpub)
const prevSigners = signers.slice(0, currentSignerIndex)
@@ -624,9 +630,16 @@ export const SignPage = () => {
setLoadingSpinnerDesc('Sending notifications')
const users = Array.from(userSet)
const promises = users.map((user) =>
- sendNotification(npubToHex(user)!, meta, fileUrl)
+ sendNotification(npubToHex(user)!, meta)
)
- await Promise.allSettled(promises)
+ await Promise.all(promises)
+ .then(() => {
+ toast.success('Notifications sent successfully')
+ setMeta(meta)
+ })
+ .catch(() => {
+ toast.error('Failed to publish notifications')
+ })
setIsLoading(false)
}
@@ -640,7 +653,7 @@ export const SignPage = () => {
}
const handleExport = async () => {
- if (!meta || !zip || !usersPubkey) return
+ if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
const usersNpub = hexToNpub(usersPubkey)
if (
@@ -653,7 +666,7 @@ export const SignPage = () => {
setIsLoading(true)
setLoadingSpinnerDesc('Signing nostr event')
- const prevSig = await getLastSignersSig()
+ const prevSig = getLastSignersSig()
if (!prevSig) return
const signedEvent = await signEventForMetaFile(
@@ -676,8 +689,15 @@ export const SignPage = () => {
null,
2
)
+
+ const zip = new JSZip()
+
zip.file('meta.json', stringifiedMeta)
+ Object.entries(files).forEach(([fileName, arrayBuffer]) => {
+ zip.file(`files/${fileName}`, arrayBuffer)
+ })
+
const arrayBuffer = await zip
.generateAsync({
type: 'arraybuffer',
@@ -705,7 +725,17 @@ export const SignPage = () => {
}
const handleExportSigit = async () => {
- if (!zip) return
+ if (Object.entries(files).length === 0 || !meta) return
+
+ const zip = new JSZip()
+
+ const stringifiedMeta = JSON.stringify(meta, null, 2)
+
+ zip.file('meta.json', stringifiedMeta)
+
+ Object.entries(files).forEach(([fileName, arrayBuffer]) => {
+ zip.file(`files/${fileName}`, arrayBuffer)
+ })
const arrayBuffer = await zip
.generateAsync({
@@ -840,11 +870,11 @@ export const SignPage = () => {
>
)}
- {submittedBy && zip && meta && (
+ {submittedBy && Object.entries(files).length > 0 && meta && (
<>
{
)}
- {/* todo: In offline mode export sigit is not visible after last signer has signed*/}
{isSignerOrCreator && (