feat: add the ability to create and sign while user is offline #85
27
src/components/copyModal.tsx
Normal file
27
src/components/copyModal.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
|
||||
import { CopyToClipboard } from './copyToClipboard'
|
||||
|
||||
interface CopyModalProps {
|
||||
open: boolean
|
||||
handleClose: () => void
|
||||
title: string
|
||||
textToCopy: string
|
||||
}
|
||||
|
||||
export const CopyModal = ({
|
||||
open,
|
||||
handleClose,
|
||||
title,
|
||||
textToCopy
|
||||
}: CopyModalProps) => {
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="xs">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<CopyToClipboard textToCopy={textToCopy} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyModal
|
51
src/components/copyToClipboard.tsx
Normal file
51
src/components/copyToClipboard.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { ContentCopy } from '@mui/icons-material/'
|
||||
import { Box, IconButton, Typography } from '@mui/material'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
type Props = {
|
||||
textToCopy: string
|
||||
}
|
||||
|
||||
export const CopyToClipboard = ({ textToCopy }: Props) => {
|
||||
const handleCopyClick = () => {
|
||||
navigator.clipboard.writeText(textToCopy)
|
||||
toast.success('Copied to clipboard', {
|
||||
autoClose: 1000,
|
||||
hideProgressBar: true
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
s marked this conversation as resolved
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopyClick()
|
||||
}}
|
||||
component="label"
|
||||
sx={{
|
||||
flex: '1',
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{textToCopy}
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopyClick()
|
||||
}}
|
||||
>
|
||||
<ContentCopy />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -58,7 +58,7 @@ export class AuthController {
|
||||
const { hostname } = window.location
|
||||
|
||||
const authEvent: EventTemplate = {
|
||||
kind: 1,
|
||||
kind: 27235,
|
||||
s marked this conversation as resolved
y
commented
it would be nice to use it would be nice to use `kinds` object from `nostr-tools`
s
commented
This change isn't related to this PR. It appeared here because staging wasn't in sync with main > it would be nice to use `kinds` object from `nostr-tools`
This change isn't related to this PR. It appeared here because staging wasn't in sync with main
|
||||
tags: [],
|
||||
content: `${hostname}-${timestamp}`,
|
||||
created_at: timestamp
|
||||
|
@ -206,15 +206,15 @@ export class MetadataController {
|
||||
|
||||
if (userRelays.length === 0) return null
|
||||
|
||||
// filter for finding user's first kind 1 event
|
||||
// filter for finding user's first kind 0 event
|
||||
const eventFilter: Filter = {
|
||||
kinds: [kinds.ShortTextNote],
|
||||
kinds: [kinds.Metadata],
|
||||
authors: [hexKey]
|
||||
}
|
||||
|
||||
const pool = new SimplePool()
|
||||
|
||||
// find user's kind 1 events published on user's relays
|
||||
// find user's kind 0 events published on user's relays
|
||||
const events = await pool.querySync(userRelays, eventFilter)
|
||||
if (events && events.length) {
|
||||
// sort events by created_at time in ascending order
|
||||
|
@ -47,11 +47,15 @@ import { DndProvider } from 'react-dnd'
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend'
|
||||
import type { Identifier, XYCoord } from 'dnd-core'
|
||||
import { useDrag, useDrop } from 'react-dnd'
|
||||
import saveAs from 'file-saver'
|
||||
import CopyModal from '../../components/copyModal'
|
||||
|
||||
export const CreatePage = () => {
|
||||
const navigate = useNavigate()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
const [openCopyModal, setOpenCopyModel] = useState(false)
|
||||
const [textToCopy, setTextToCopy] = useState('')
|
||||
|
||||
const [authUrl, setAuthUrl] = useState<string>()
|
||||
|
||||
@ -329,10 +333,12 @@ export const CreatePage = () => {
|
||||
const encryptedArrayBuffer = await encryptArrayBuffer(
|
||||
arraybuffer,
|
||||
encryptionKey
|
||||
)
|
||||
).finally(() => setIsLoading(false))
|
||||
|
||||
const blob = new Blob([encryptedArrayBuffer])
|
||||
|
||||
if (navigator.onLine) {
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Uploading zip file to file storage.')
|
||||
const fileUrl = await uploadToFileStorage(blob, nostrController)
|
||||
.then((url) => {
|
||||
@ -381,6 +387,11 @@ export const CreatePage = () => {
|
||||
fileUrl
|
||||
)}&key=${encodeURIComponent(encryptionKey)}`
|
||||
)
|
||||
} else {
|
||||
saveAs(blob, 'request.sigit')
|
||||
setTextToCopy(encryptionKey)
|
||||
setOpenCopyModel(true)
|
||||
}
|
||||
}
|
||||
|
||||
if (authUrl) {
|
||||
@ -471,6 +482,15 @@ export const CreatePage = () => {
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<CopyModal
|
||||
open={openCopyModal}
|
||||
handleClose={() => {
|
||||
setOpenCopyModel(false)
|
||||
navigate(appPrivateRoutes.sign)
|
||||
}}
|
||||
title="Decryption key for Sigit file"
|
||||
textToCopy={textToCopy}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ import {
|
||||
Download,
|
||||
HourglassTop
|
||||
} from '@mui/icons-material'
|
||||
|
||||
import CopyModal from '../../components/copyModal'
|
||||
enum SignedStatus {
|
||||
Fully_Signed,
|
||||
User_Is_Next_Signer,
|
||||
@ -79,6 +79,8 @@ export const SignPage = () => {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
const [openCopyModal, setOpenCopyModel] = useState(false)
|
||||
const [textToCopy, setTextToCopy] = useState('')
|
||||
|
||||
const [meta, setMeta] = useState<Meta | null>(null)
|
||||
const [signedStatus, setSignedStatus] = useState<SignedStatus>()
|
||||
@ -98,6 +100,9 @@ export const SignPage = () => {
|
||||
|
||||
const [nextSinger, setNextSinger] = useState<string>()
|
||||
|
||||
// This state variable indicates whether the logged-in user is a signer, a creator, or neither.
|
||||
const [isSignerOrCreator, setIsSignerOrCreator] = useState(false)
|
||||
|
||||
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||
|
||||
const [authUrl, setAuthUrl] = useState<string>()
|
||||
@ -167,7 +172,20 @@ export const SignPage = () => {
|
||||
// there's no signer just viewers. So its fully signed
|
||||
setSignedStatus(SignedStatus.Fully_Signed)
|
||||
}
|
||||
}, [signers, signedBy, usersPubkey])
|
||||
|
||||
// Determine and set the status of the user
|
||||
if (submittedBy && usersPubkey && submittedBy === usersPubkey) {
|
||||
// If the submission was made by the user, set the status to true
|
||||
setIsSignerOrCreator(true)
|
||||
} else if (usersPubkey) {
|
||||
// Convert the user's public key from hex to npub format
|
||||
const usersNpub = hexToNpub(usersPubkey)
|
||||
if (signers.includes(usersNpub)) {
|
||||
// If the user's npub is in the list of signers, set the status to true
|
||||
setIsSignerOrCreator(true)
|
||||
}
|
||||
}
|
||||
}, [signers, signedBy, usersPubkey, submittedBy])
|
||||
|
||||
useEffect(() => {
|
||||
const fileUrl = searchParams.get('file')
|
||||
@ -236,6 +254,7 @@ export const SignPage = () => {
|
||||
if (!zip) return
|
||||
|
||||
setZip(zip)
|
||||
setDisplayInput(false)
|
||||
|
||||
setLoadingSpinnerDesc('Parsing meta.json')
|
||||
|
||||
@ -414,6 +433,7 @@ export const SignPage = () => {
|
||||
|
||||
const blob = new Blob([encryptedArrayBuffer])
|
||||
|
||||
if (navigator.onLine) {
|
||||
setLoadingSpinnerDesc('Uploading zip file to file storage.')
|
||||
const fileUrl = await uploadToFileStorage(blob, nostrController)
|
||||
.then((url) => {
|
||||
@ -483,6 +503,9 @@ export const SignPage = () => {
|
||||
file: fileUrl,
|
||||
key: key
|
||||
})
|
||||
} else {
|
||||
handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false))
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
@ -549,6 +572,37 @@ export const SignPage = () => {
|
||||
navigate(appPrivateRoutes.verify)
|
||||
}
|
||||
|
||||
const handleExportSigit = async () => {
|
||||
if (!zip) return
|
||||
|
||||
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()
|
||||
|
||||
setLoadingSpinnerDesc('Encrypting zip file')
|
||||
const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key)
|
||||
const blob = new Blob([encryptedArrayBuffer])
|
||||
saveAs(blob, 'exported.sigit')
|
||||
|
||||
setTextToCopy(key)
|
||||
setOpenCopyModel(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function accepts an npub of a signer and return the signature of its previous signer.
|
||||
* This prevSig will be used in the content of the provided signer's signedEvent
|
||||
@ -637,6 +691,7 @@ export const SignPage = () => {
|
||||
<Box className={styles.inputBlock}>
|
||||
<MuiFileInput
|
||||
placeholder="Select file"
|
||||
inputProps={{ accept: '.sigit' }}
|
||||
value={selectedFile}
|
||||
onChange={(value) => setSelectedFile(value)}
|
||||
/>
|
||||
@ -675,6 +730,7 @@ export const SignPage = () => {
|
||||
nextSigner={nextSinger}
|
||||
getPrevSignersSig={getPrevSignersSig}
|
||||
/>
|
||||
|
||||
{signedStatus === SignedStatus.Fully_Signed && (
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleExport} variant="contained">
|
||||
@ -690,9 +746,24 @@ export const SignPage = () => {
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isSignerOrCreator &&
|
||||
signedStatus === SignedStatus.User_Is_Not_Next_Signer && (
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleExportSigit} variant="contained">
|
||||
Export Sigit
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<CopyModal
|
||||
open={openCopyModal}
|
||||
handleClose={() => setOpenCopyModel(false)}
|
||||
title="Decryption key for Sigit file"
|
||||
textToCopy={textToCopy}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -189,7 +189,7 @@ export const signEventForMetaFile = async (
|
||||
) => {
|
||||
// Construct the event metadata for the meta file
|
||||
const event: EventTemplate = {
|
||||
kind: 1, // Event type for meta file
|
||||
kind: 27235, // Event type for meta file
|
||||
content: content, // content for event
|
||||
created_at: Math.floor(Date.now() / 1000), // Current timestamp
|
||||
tags: []
|
||||
|
Loading…
Reference in New Issue
Block a user
it would be nice to move css logic into separate file
This is a simple utility component, I think it's fine to add inline style in this case. Otherwise, I'll create a folder and separate .scss file just for 1 class