import { Box, Button, FormControl, IconButton, InputLabel, List, ListItem, ListSubheader, MenuItem, Select, TextField, Typography } from '@mui/material' import { MuiFileInput } from 'mui-file-input' import styles from './style.module.scss' import { Dispatch, SetStateAction, useEffect, useState } from 'react' import placeholderAvatar from '../../assets/images/nostr-logo.jpg' import { ProfileMetadata } from '../../types' import { MetadataController, NostrController } from '../../controllers' import { Link } from 'react-router-dom' import { encryptArrayBuffer, generateEncryptionKey, getFileHash, pubToHex, queryNip05, shorten } from '../../utils' import { LoadingSpinner } from '../../components/LoadingSpinner' import { getProfileRoute } from '../../routes' import { Clear } from '@mui/icons-material' import JSZip from 'jszip' import { toast } from 'react-toastify' import { useSelector } from 'react-redux' import { State } from '../../store/rootReducer' import { EventTemplate } from 'nostr-tools' import axios from 'axios' enum SelectionType { signer = 'Signer', viewer = 'Viewer' } type MetadataMap = { [key: string]: ProfileMetadata } export const HomePage = () => { const [inputValue, setInputValue] = useState('') const [type, setType] = useState(SelectionType.signer) const [error, setError] = useState() const [signers, setSigners] = useState([]) const [viewers, setViewers] = useState([]) const [metadataMap, setMetadataMap] = useState({}) const [selectedFiles, setSelectedFiles] = useState([]) const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [authUrl, setAuthUrl] = useState() const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const nostrController = NostrController.getInstance() const handleAddClick = async () => { setError(undefined) const addPubkey = (pubkey: string) => { const addElement = (prev: string[]) => { // if key is already in the list just return that if (prev.includes(pubkey)) return prev return [...prev, pubkey] } if (type === SelectionType.signer) { setSigners(addElement) } else { setViewers(addElement) } } if (inputValue.startsWith('npub')) { const pubkey = await pubToHex(inputValue) if (pubkey) { addPubkey(pubkey) setInputValue('') } else { setError('Provided npub is not valid. Please enter correct npub.') } return } if (inputValue.includes('@')) { setIsLoading(true) setLoadingSpinnerDesc('Querying for nip05') const nip05Profile = await queryNip05(inputValue) .catch((err) => { console.error(`error occurred in querying nip05: ${inputValue}`, err) return null }) .finally(() => { setIsLoading(false) setLoadingSpinnerDesc('') }) if (nip05Profile) { const pubkey = nip05Profile.pubkey addPubkey(pubkey) setInputValue('') } else { setError('Provided nip05 is not valid. Please enter correct nip05.') } return } setError('Invalid input! Make sure to provide correct npub or nip05.') } const handleRemove = (pubkey: string, selectionType: SelectionType) => { if (selectionType === SelectionType.signer) { setSigners((prev) => prev.filter((signer) => signer !== pubkey)) } else { setViewers((prev) => prev.filter((viewer) => viewer !== pubkey)) } } const handleSelectFiles = (files: File[]) => { 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 handleSubmit = async () => { if (signers.length === 0) { toast.error('No signer is provided. At least provide one signer.') return } if (viewers.length === 0) { toast.error('No viewer is provided. At least provide one viewer.') return } if (selectedFiles.length === 0) { toast.error('No file is provided. At least provide one file.') return } setIsLoading(true) setLoadingSpinnerDesc('Generating hashes for files') const fileHashes: { [key: string]: string } = {} for (const file of selectedFiles) { const hash = await getFileHash(file) fileHashes[file.name] = hash } const zip = new JSZip() selectedFiles.forEach((file) => { zip.file(`files/${file.name}`, file) }) const event: EventTemplate = { kind: 1, tags: [['r', signers[0]]], content: JSON.stringify(fileHashes), created_at: Math.floor(Date.now() / 1000) } setLoadingSpinnerDesc('Signing nostr event') const signedEvent = await nostrController.signEvent(event).catch((err) => { console.error(err) toast.error(err.message || 'Error occurred in signing nostr event') setIsLoading(false) return null }) if (!signedEvent) return const meta = { signers, viewers, fileHashes, submittedBy: usersPubkey, signedEvents: { [signedEvent.pubkey]: JSON.stringify(signedEvent, null, 2) } } try { const stringifiedMeta = JSON.stringify(meta, null, 2) zip.file('meta.json', stringifiedMeta) } catch (err) { toast.error('An error occurred in converting meta json to string') } 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) .then((url) => { toast.success('zip file uploaded to file storage') return url }) .catch((err) => { console.log('err in upload:>> ', err) toast.error(err.message || 'Error occurred in uploading zip file') return null }) if (!fileUrl) return await sendDMToFirstSigner(fileUrl, encryptionKey, signers[0]) setIsLoading(false) } const uploadToFileStorage = async (blob: Blob) => { const unixNow = Math.floor(Date.now() / 1000) const file = new File([blob], `zipped-${unixNow}.zip`, { type: 'application/zip' }) const event: EventTemplate = { kind: 24242, content: 'Authorize Upload', created_at: Math.floor(Date.now() / 1000), tags: [ ['t', 'upload'], ['expiration', String(unixNow + 60 * 5)], ['name', file.name], ['size', String(file.size)] ] } setLoadingSpinnerDesc('Signing auth event for uploading zip') const authEvent = await nostrController.signEvent(event) const FILE_STORAGE_URL = 'https://blossom.sigit.io' const response = await axios.put(`${FILE_STORAGE_URL}/upload`, file, { headers: { Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)), 'Content-Type': 'application/zip' } }) return response.data.url as string } const sendDMToFirstSigner = async ( fileUrl: string, encryptionKey: string, pubkey: string ) => { const content = `You have been requested for a signature.\nHere is the url for zip file that you can download.\n ${fileUrl}\nHowever this zip file is encrypted and you need to decrypt it using https://app.sigit.io\n Encryption key: ${encryptionKey}` nostrController.on('nsecbunker-auth', (url) => { setAuthUrl(url) }) setLoadingSpinnerDesc('encrypting content for DM') // todo: add timeout const encrypted = await nostrController .nip04Encrypt(pubkey, content) .then((res) => { return res }) .catch((err) => { console.log('err :>> ', err) toast.error( err.message || 'An error occurred while encrypting DM content' ) return null }) .finally(() => { setAuthUrl(undefined) }) if (!encrypted) return const event: EventTemplate = { kind: 4, content: encrypted, created_at: Math.floor(Date.now() / 1000), tags: [['p', signers[0]]] } setLoadingSpinnerDesc('signing event for DM') const signedEvent = await nostrController.signEvent(event).catch((err) => { console.log('err :>> ', err) toast.error(err.message || 'An error occurred while signing event for DM') return null }) if (!signedEvent) return // const metadata = metadataMap[pubkey] setLoadingSpinnerDesc('Publishing encrypted DM') // todo: do not use hardcoded relay await nostrController .publishEvent(signedEvent, 'wss://relayable.org') .then(() => { toast.success('DM sent to first signer') }) .catch((err) => { console.log('err :>> ', err) toast.error(err.message || 'An error occurred while publishing DM') return null }) } if (authUrl) { return (