import { Clear } from '@mui/icons-material' import { Box, Button, FormControl, IconButton, InputLabel, MenuItem, Paper, Select, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Tooltip, Typography } from '@mui/material' import { MuiFileInput } from 'mui-file-input' import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import placeholderAvatar from '../../assets/images/nostr-logo.jpg' import { LoadingSpinner } from '../../components/LoadingSpinner' import { MetadataController, NostrController } from '../../controllers' import { getProfileRoute } from '../../routes' import { ProfileMetadata, User, UserRole } from '../../types' import { encryptArrayBuffer, generateEncryptionKey, getHash, hexToNpub, pubToHex, queryNip05, sendDM, shorten, signEventForMetaFile, uploadToFileStorage } from '../../utils' import styles from './style.module.scss' import { toast } from 'react-toastify' import JSZip from 'jszip' import { useSelector } from 'react-redux' import { State } from '../../store/rootReducer' export const SignPage = () => { const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [authUrl, setAuthUrl] = useState() const [selectedFiles, setSelectedFiles] = useState([]) const [displayUserInput, setDisplayUserInput] = useState(false) const [userInput, setUserInput] = useState('') const [userRole, setUserRole] = useState(UserRole.signer) const [error, setError] = useState() const [users, setUsers] = useState([]) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const nostrController = NostrController.getInstance() const handleAddUser = async () => { setError(undefined) const addUser = (pubkey: string) => { setUsers((prev) => { const existingUserIndex = prev.findIndex( (user) => user.pubkey === pubkey ) // add new if (existingUserIndex === -1) return [...prev, { pubkey, role: userRole }] const existingUser = prev[existingUserIndex] // return existing if (existingUser.role === userRole) return prev // change user role const updatedUsers = [...prev] const updatedUser = { ...updatedUsers[existingUserIndex] } updatedUser.role = userRole updatedUsers[existingUserIndex] = updatedUser return updatedUsers }) setUserInput('') } if (userInput.startsWith('npub')) { const pubkey = await pubToHex(userInput) if (pubkey) { addUser(pubkey) setUserInput('') } else { setError('Provided npub is not valid. Please enter correct npub.') } return } if (userInput.includes('@')) { setIsLoading(true) setLoadingSpinnerDesc('Querying for nip05') const nip05Profile = await queryNip05(userInput) .catch((err) => { console.error(`error occurred in querying nip05: ${userInput}`, err) return null }) .finally(() => { setIsLoading(false) setLoadingSpinnerDesc('') }) if (nip05Profile) { const pubkey = nip05Profile.pubkey addUser(pubkey) setUserInput('') } else { setError('Provided nip05 is not valid. Please enter correct nip05.') } return } setError('Invalid input! Make sure to provide correct npub or nip05.') } const handleUserRoleChange = (role: UserRole, index: number) => { setUsers((prevUsers) => { // Create a shallow copy of the previous state const updatedUsers = [...prevUsers] // Create a shallow copy of the user object at the specified index const updatedUser = { ...updatedUsers[index] } // Update the role property of the copied user object updatedUser.role = role // Update the user object at the specified index in the copied array updatedUsers[index] = updatedUser // Return the updated array return updatedUsers }) } const handleRemoveUser = (pubkey: string) => { setUsers((prev) => prev.filter((user) => user.pubkey !== pubkey)) } const handleSelectFiles = (files: File[]) => { setDisplayUserInput(true) setSelectedFiles((prev) => { const prevFileNames = prev.map((file) => file.name) const newFiles = files.filter( (file) => !prevFileNames.includes(file.name) ) return [...prev, ...newFiles] }) } const handleRemoveFile = (fileToRemove: File) => { setSelectedFiles((prevFiles) => prevFiles.filter((file) => file.name !== fileToRemove.name) ) } const handleSign = async () => { if (users.length === 0) { toast.error( 'No signer/viewer is provided. At least add one signer or viewer.' ) return } if (selectedFiles.length === 0) { toast.error('No file is selected. Select at least 1 file') return } setIsLoading(true) setLoadingSpinnerDesc('Generating hashes for files') const fileHashes: { [key: string]: string } = {} // generating file hashes for (const file of selectedFiles) { const arraybuffer = await file.arrayBuffer().catch((err) => { console.log( `err while getting arrayBuffer of file ${file.name} :>> `, err ) toast.error( err.message || `err while getting arrayBuffer of file ${file.name}` ) return null }) if (!arraybuffer) return const hash = await getHash(arraybuffer) if (!hash) { setIsLoading(false) return } fileHashes[file.name] = hash } const zip = new JSZip() // zipping files selectedFiles.forEach((file) => { zip.file(`files/${file.name}`, file) }) const signers = users.filter((user) => user.role === UserRole.signer) const viewers = users.filter((user) => user.role === UserRole.viewer) setLoadingSpinnerDesc('Signing nostr event') const signedEvent = await signEventForMetaFile( fileHashes, nostrController, setIsLoading ) if (!signedEvent) return // create content for meta file const meta = { signers: signers.map((signer) => signer.pubkey), viewers: viewers.map((viewer) => viewer.pubkey), fileHashes, submittedBy: usersPubkey, signedEvents: { [signedEvent.pubkey]: JSON.stringify(signedEvent, null, 2) } } try { const stringifiedMeta = JSON.stringify(meta, null, 2) zip.file('meta.json', stringifiedMeta) const metaHash = await getHash(stringifiedMeta) if (!metaHash) return const metaHashJson = { [usersPubkey!]: metaHash } zip.file('hashes.json', JSON.stringify(metaHashJson, null, 2)) } catch (err) { console.error(err) toast.error('An error occurred in converting meta json to string') return } setLoadingSpinnerDesc('Generating zip file') 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 encryptionKey = await generateEncryptionKey() setLoadingSpinnerDesc('Encrypting zip file') const encryptedArrayBuffer = await encryptArrayBuffer( arraybuffer, encryptionKey ) 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 setLoadingSpinnerDesc('Sending DM to signers/viewers') // send DM to first signer if exists if (signers.length > 0) { await sendDM( fileUrl, encryptionKey, signers[0].pubkey, nostrController, true, setAuthUrl ) } else { // send DM to all viewers if no signer for (const viewer of viewers) { // todo: execute in parallel await sendDM( fileUrl, encryptionKey, viewer.pubkey, nostrController, false, setAuthUrl ) } } setIsLoading(false) } if (authUrl) { return (