chore(git): Merge branch 'staging' into issue-274
This commit is contained in:
commit
efc8b9f37a
49
package-lock.json
generated
49
package-lock.json
generated
@ -31,8 +31,9 @@
|
||||
"idb": "8.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"lodash": "4.17.21",
|
||||
"material-ui-popup-state": "^5.3.1",
|
||||
"mui-file-input": "4.0.4",
|
||||
"nostr-login": "^1.6.6",
|
||||
"nostr-login": "1.6.14",
|
||||
"nostr-tools": "2.7.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^4.4.168",
|
||||
@ -3330,6 +3331,12 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||
@ -3580,10 +3587,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@ -5976,6 +5984,26 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/material-ui-popup-state": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-5.3.1.tgz",
|
||||
"integrity": "sha512-mmx1DsQwF/2cmcpHvS/QkUwOQG2oAM+cDEQU0DaZVYnvwKyTB3AFgu8l1/E+LQFausmzpSJoljwQSZXkNvt7eA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.6",
|
||||
"@types/prop-types": "^15.7.3",
|
||||
"@types/react": "^18.0.26",
|
||||
"classnames": "^2.2.6",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mui/material": "^5.0.0 || ^6.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/md5.js": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||
@ -6227,9 +6255,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -6237,6 +6265,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@ -6415,9 +6444,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nostr-login": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/nostr-login/-/nostr-login-1.6.6.tgz",
|
||||
"integrity": "sha512-XOpB9nG3Qgt7iea7gA1zn4TaTfUKCKGdCHKwErqLPtMk/q1Rhkzj5cq/66iU0WqC6mSiwENfTy1p4qaM7HzMtg==",
|
||||
"version": "1.6.14",
|
||||
"resolved": "https://registry.npmjs.org/nostr-login/-/nostr-login-1.6.14.tgz",
|
||||
"integrity": "sha512-pId1G79kjRW1B9qy6OrA8Not23JSfgmS2VegcKf7Qm9VMC7wYGXg1Ry3FMEAB8p11WoboQ8oJi2TqUGiOf61OQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nostr-dev-kit/ndk": "^2.3.1",
|
||||
|
@ -41,8 +41,9 @@
|
||||
"idb": "8.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"lodash": "4.17.21",
|
||||
"material-ui-popup-state": "^5.3.1",
|
||||
"mui-file-input": "4.0.4",
|
||||
"nostr-login": "^1.6.6",
|
||||
"nostr-login": "1.6.14",
|
||||
"nostr-tools": "2.7.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^4.4.168",
|
||||
|
@ -125,7 +125,7 @@ export const AppBar = () => {
|
||||
src="/logo.svg"
|
||||
alt="Logo"
|
||||
onClick={() => {
|
||||
if (window.location.pathname === '/') {
|
||||
if (['', '#/'].includes(window.location.hash)) {
|
||||
location.reload()
|
||||
} else {
|
||||
navigate('/')
|
||||
|
@ -1,23 +1,25 @@
|
||||
import { CurrentUserFile } from '../../types/file.ts'
|
||||
import styles from './style.module.scss'
|
||||
import { Button } from '@mui/material'
|
||||
import { Button, Menu, MenuItem } from '@mui/material'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state'
|
||||
import React from 'react'
|
||||
|
||||
interface FileListProps {
|
||||
files: CurrentUserFile[]
|
||||
currentFile: CurrentUserFile
|
||||
setCurrentFile: (file: CurrentUserFile) => void
|
||||
handleDownload: () => void
|
||||
downloadLabel?: string
|
||||
handleExport: () => void
|
||||
handleEncryptedExport?: () => void
|
||||
}
|
||||
|
||||
const FileList = ({
|
||||
files,
|
||||
currentFile,
|
||||
setCurrentFile,
|
||||
handleDownload,
|
||||
downloadLabel
|
||||
handleExport,
|
||||
handleEncryptedExport
|
||||
}: FileListProps) => {
|
||||
const isActive = (file: CurrentUserFile) => file.id === currentFile.id
|
||||
return (
|
||||
@ -42,9 +44,35 @@ const FileList = ({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button variant="contained" fullWidth onClick={handleDownload}>
|
||||
{downloadLabel || 'Download Files'}
|
||||
</Button>
|
||||
|
||||
<PopupState variant="popover" popupId="download-popup-menu">
|
||||
{(popupState) => (
|
||||
<React.Fragment>
|
||||
<Button variant="contained" {...bindTrigger(popupState)}>
|
||||
Export files
|
||||
</Button>
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
popupState.close
|
||||
handleExport()
|
||||
}}
|
||||
>
|
||||
Export Files
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
popupState.close
|
||||
typeof handleEncryptedExport === 'function' &&
|
||||
handleEncryptedExport()
|
||||
}}
|
||||
>
|
||||
Export Encrypted Files
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</PopupState>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ export const Footer = () =>
|
||||
component={Link}
|
||||
to={'/'}
|
||||
onClick={(event) => {
|
||||
if (window.location.pathname === '/') {
|
||||
if (['', '#/'].includes(window.location.hash)) {
|
||||
event.preventDefault()
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { CurrentUserMark } from '../../types/mark.ts'
|
||||
import styles from './style.module.scss'
|
||||
import {
|
||||
findNextIncompleteCurrentUserMark,
|
||||
getToolboxLabelByMarkType,
|
||||
@ -8,12 +7,16 @@ import {
|
||||
} from '../../utils'
|
||||
import React, { useState } from 'react'
|
||||
import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button } from '@mui/material'
|
||||
import styles from './style.module.scss'
|
||||
|
||||
interface MarkFormFieldProps {
|
||||
currentUserMarks: CurrentUserMark[]
|
||||
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
|
||||
handleSelectedMarkValueChange: (value: string) => void
|
||||
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||
handleSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||
selectedMark: CurrentUserMark
|
||||
selectedMarkValue: string
|
||||
}
|
||||
@ -30,11 +33,13 @@ const MarkFormField = ({
|
||||
handleCurrentUserMarkChange
|
||||
}: MarkFormFieldProps) => {
|
||||
const [displayActions, setDisplayActions] = useState(true)
|
||||
const [complete, setComplete] = useState(false)
|
||||
|
||||
const isReadyToSign = () =>
|
||||
isCurrentUserMarksComplete(currentUserMarks) ||
|
||||
isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue)
|
||||
const isCurrent = (currentMark: CurrentUserMark) =>
|
||||
currentMark.id === selectedMark.id
|
||||
currentMark.id === selectedMark.id && !complete
|
||||
const isDone = (currentMark: CurrentUserMark) =>
|
||||
isCurrent(currentMark) ? !!selectedMarkValue : currentMark.isCompleted
|
||||
const findNext = () => {
|
||||
@ -46,13 +51,36 @@ const MarkFormField = ({
|
||||
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
console.log('handle form submit runs...')
|
||||
return isReadyToSign()
|
||||
? handleSubmit(event)
|
||||
: handleCurrentUserMarkChange(findNext()!)
|
||||
|
||||
// Without this line, we lose mark values when switching
|
||||
handleCurrentUserMarkChange(selectedMark)
|
||||
|
||||
if (!complete) {
|
||||
isReadyToSign()
|
||||
? setComplete(true)
|
||||
: handleCurrentUserMarkChange(findNext()!)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleActions = () => setDisplayActions(!displayActions)
|
||||
const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type)
|
||||
|
||||
const handleCurrentUserMarkClick = (mark: CurrentUserMark) => {
|
||||
setComplete(false)
|
||||
handleCurrentUserMarkChange(mark)
|
||||
}
|
||||
|
||||
const handleSelectCompleteMark = () => {
|
||||
handleCurrentUserMarkChange(selectedMark)
|
||||
setComplete(true)
|
||||
}
|
||||
|
||||
const handleSignAndComplete = (
|
||||
event: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
handleSubmit(event)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.trigger}>
|
||||
@ -78,33 +106,55 @@ const MarkFormField = ({
|
||||
<div className={styles.actionsWrapper}>
|
||||
<div className={styles.actionsTop}>
|
||||
<div className={styles.actionsTopInfo}>
|
||||
<p className={styles.actionsTopInfoText}>Add {markLabel}</p>
|
||||
{!complete && (
|
||||
<p className={styles.actionsTopInfoText}>Add {markLabel}</p>
|
||||
)}
|
||||
{complete && <p className={styles.actionsTopInfoText}>Finish</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.inputWrapper}>
|
||||
<form onSubmit={(e) => handleFormSubmit(e)}>
|
||||
<MarkInput
|
||||
markType={selectedMark.mark.type}
|
||||
key={selectedMark.id}
|
||||
value={selectedMarkValue}
|
||||
placeholder={markLabel}
|
||||
handler={handleSelectedMarkValueChange}
|
||||
userMark={selectedMark}
|
||||
/>
|
||||
{!complete && (
|
||||
<form onSubmit={(e) => handleFormSubmit(e)}>
|
||||
<MarkInput
|
||||
markType={selectedMark.mark.type}
|
||||
key={selectedMark.id}
|
||||
value={selectedMarkValue}
|
||||
placeholder={markLabel}
|
||||
handler={handleSelectedMarkValueChange}
|
||||
userMark={selectedMark}
|
||||
/>
|
||||
<div className={styles.actionsBottom}>
|
||||
<Button type="submit" className={styles.submitButton}>
|
||||
NEXT
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{complete && (
|
||||
<div className={styles.actionsBottom}>
|
||||
<button type="submit" className={styles.submitButton}>
|
||||
NEXT
|
||||
</button>
|
||||
<Button
|
||||
onClick={handleSignAndComplete}
|
||||
className={[styles.submitButton, styles.completeButton].join(
|
||||
' '
|
||||
)}
|
||||
disabled={!isReadyToSign()}
|
||||
autoFocus
|
||||
>
|
||||
SIGN AND COMPLETE
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className={styles.footerContainer}>
|
||||
<div className={styles.footer}>
|
||||
{currentUserMarks.map((mark, index) => {
|
||||
return (
|
||||
<div className={styles.pagination} key={index}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.paginationButton} ${isDone(mark) ? styles.paginationButtonDone : ''}`}
|
||||
onClick={() => handleCurrentUserMarkChange(mark)}
|
||||
onClick={() => handleCurrentUserMarkClick(mark)}
|
||||
>
|
||||
{mark.id}
|
||||
</button>
|
||||
@ -114,6 +164,22 @@ const MarkFormField = ({
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className={styles.pagination}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.paginationButton} ${isReadyToSign() ? styles.paginationButtonDone : ''}`}
|
||||
onClick={handleSelectCompleteMark}
|
||||
title="Complete"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className={styles.finishPage}
|
||||
icon={faCheck}
|
||||
/>
|
||||
</button>
|
||||
{complete && (
|
||||
<div className={styles.paginationButtonCurrent}></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -70,6 +70,11 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.completeButton {
|
||||
font-size: 18px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.paginationButton {
|
||||
font-size: 12px;
|
||||
padding: 5px 10px;
|
||||
@ -78,7 +83,8 @@
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.paginationButton:hover {
|
||||
.paginationButton:hover,
|
||||
.paginationButton:focus {
|
||||
background: #447592;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
@ -216,3 +222,7 @@
|
||||
flex-direction: column;
|
||||
grid-gap: 5px;
|
||||
}
|
||||
|
||||
.finishPage {
|
||||
padding: 1px 0;
|
||||
}
|
||||
|
@ -24,11 +24,12 @@ import {
|
||||
interface PdfMarkingProps {
|
||||
currentUserMarks: CurrentUserMark[]
|
||||
files: CurrentUserFile[]
|
||||
handleDownload: () => void
|
||||
handleExport: () => void
|
||||
handleEncryptedExport: () => void
|
||||
handleSign: () => void
|
||||
meta: Meta | null
|
||||
otherUserMarks: Mark[]
|
||||
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
|
||||
setIsMarksCompleted: (isMarksCompleted: boolean) => void
|
||||
setUpdatedMarks: (markToUpdate: Mark) => void
|
||||
}
|
||||
|
||||
@ -42,10 +43,11 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
||||
const {
|
||||
files,
|
||||
currentUserMarks,
|
||||
setIsMarksCompleted,
|
||||
setCurrentUserMarks,
|
||||
setUpdatedMarks,
|
||||
handleDownload,
|
||||
handleExport,
|
||||
handleEncryptedExport,
|
||||
handleSign,
|
||||
meta,
|
||||
otherUserMarks
|
||||
} = props
|
||||
@ -86,11 +88,18 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
||||
updatedSelectedMark
|
||||
)
|
||||
setCurrentUserMarks(updatedCurrentUserMarks)
|
||||
setSelectedMarkValue(mark.currentValue ?? EMPTY)
|
||||
setSelectedMark(mark)
|
||||
|
||||
// If clicking on the same mark, don't update the value, otherwise do update
|
||||
if (mark.id !== selectedMark.id) {
|
||||
setSelectedMarkValue(mark.currentValue ?? EMPTY)
|
||||
setSelectedMark(mark)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
/**
|
||||
* Sign and Complete
|
||||
*/
|
||||
const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
if (!selectedMarkValue || !selectedMark) return
|
||||
|
||||
@ -106,8 +115,8 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
||||
)
|
||||
setCurrentUserMarks(updatedCurrentUserMarks)
|
||||
setSelectedMark(null)
|
||||
setIsMarksCompleted(true)
|
||||
setUpdatedMarks(updatedMark.mark)
|
||||
handleSign()
|
||||
}
|
||||
|
||||
// const updateCurrentUserMarkValues = () => {
|
||||
@ -132,7 +141,8 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
||||
files={files}
|
||||
currentFile={currentFile}
|
||||
setCurrentFile={setCurrentFile}
|
||||
handleDownload={handleDownload}
|
||||
handleExport={handleExport}
|
||||
handleEncryptedExport={handleEncryptedExport}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -45,7 +45,7 @@ export interface FlatMeta
|
||||
isValid: boolean
|
||||
|
||||
// Decryption
|
||||
encryptionKey: string | null
|
||||
encryptionKey: string | undefined
|
||||
|
||||
// Parsed Document Signatures
|
||||
parsedSignatureEvents: {
|
||||
@ -101,7 +101,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
[signer: `npub1${string}`]: SignStatus
|
||||
}>({})
|
||||
|
||||
const [encryptionKey, setEncryptionKey] = useState<string | null>(null)
|
||||
const [encryptionKey, setEncryptionKey] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!meta) return
|
||||
@ -143,7 +143,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
setMarkConfig(markConfig)
|
||||
setZipUrl(zipUrl)
|
||||
|
||||
let encryptionKey: string | null = null
|
||||
let encryptionKey: string | undefined
|
||||
if (meta.keys) {
|
||||
const { sender, keys } = meta.keys
|
||||
// Retrieve the user's public key from the state
|
||||
@ -161,7 +161,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
'An error occurred in decrypting encryption key',
|
||||
err
|
||||
)
|
||||
return null
|
||||
return undefined
|
||||
})
|
||||
|
||||
encryptionKey = decrypted
|
||||
|
@ -134,7 +134,15 @@ export const MainLayout = () => {
|
||||
initNostrLogin({
|
||||
methods: ['connect', 'extension', 'local'],
|
||||
noBanner: true,
|
||||
onAuth: handleNostrAuth
|
||||
onAuth: handleNostrAuth,
|
||||
outboxRelays: [
|
||||
'wss://purplepag.es',
|
||||
'wss://relay.nos.social',
|
||||
'wss://user.kindpag.es',
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.sigit.io'
|
||||
]
|
||||
}).catch((error) => {
|
||||
console.error('Failed to initialize Nostr-Login', error)
|
||||
})
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
KeyboardCode,
|
||||
Meta,
|
||||
ProfileMetadata,
|
||||
SigitNotification,
|
||||
SignedEvent,
|
||||
User,
|
||||
UserRole
|
||||
@ -52,7 +53,9 @@ import {
|
||||
updateUsersAppData,
|
||||
uploadToFileStorage,
|
||||
DEFAULT_TOOLBOX,
|
||||
settleAllFullfilfedPromises
|
||||
settleAllFullfilfedPromises,
|
||||
DEFAULT_LOOK_UP_RELAY_LIST,
|
||||
uploadMetaToFileStorage
|
||||
} from '../../utils'
|
||||
import { Container } from '../../components/Container'
|
||||
import fileListStyles from '../../components/FileList/style.module.scss'
|
||||
@ -148,6 +151,17 @@ export const CreatePage = () => {
|
||||
[setUserInput]
|
||||
)
|
||||
|
||||
const handleSearchUserNip05 = async (
|
||||
nip05: string
|
||||
): Promise<string | null> => {
|
||||
const { pubkey } = await queryNip05(nip05).catch((err) => {
|
||||
console.error(err)
|
||||
return { pubkey: null }
|
||||
})
|
||||
|
||||
return pubkey
|
||||
}
|
||||
|
||||
const handleSearchUsers = async (searchValue?: string) => {
|
||||
const searchString = searchValue || userSearchInput || undefined
|
||||
|
||||
@ -159,6 +173,14 @@ export const CreatePage = () => {
|
||||
const metadataController = MetadataController.getInstance()
|
||||
|
||||
const relaySet = await metadataController.findRelayListMetadata(usersPubkey)
|
||||
|
||||
DEFAULT_LOOK_UP_RELAY_LIST.forEach((relay) => {
|
||||
if (!relaySet.write.includes(relay)) relaySet.write.push(relay)
|
||||
if (!relaySet.read.includes(relay)) relaySet.read.push(relay)
|
||||
})
|
||||
|
||||
const uniqueReadRelaySet = [...new Set(relaySet.read)]
|
||||
|
||||
const searchTerm = searchString.trim()
|
||||
|
||||
relayController
|
||||
@ -167,7 +189,7 @@ export const CreatePage = () => {
|
||||
kinds: [0],
|
||||
search: searchTerm
|
||||
},
|
||||
[...relaySet.write]
|
||||
uniqueReadRelaySet
|
||||
)
|
||||
.then((events) => {
|
||||
console.log('events', events)
|
||||
@ -224,7 +246,9 @@ export const CreatePage = () => {
|
||||
})
|
||||
}, [foundUsers])
|
||||
|
||||
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const handleInputKeyDown = async (
|
||||
event: React.KeyboardEvent<HTMLDivElement>
|
||||
) => {
|
||||
if (
|
||||
event.code === KeyboardCode.Enter ||
|
||||
event.code === KeyboardCode.NumpadEnter
|
||||
@ -238,7 +262,23 @@ export const CreatePage = () => {
|
||||
} else {
|
||||
// Otherwize if search already provided some results, user must manually click the search button
|
||||
if (!foundUsers.length) {
|
||||
handleSearchUsers()
|
||||
// If it's NIP05 (includes @ or is a valid domain) send request to .well-known
|
||||
const domainRegex = /^[a-zA-Z0-9@.-]+\.[a-zA-Z]{2,}$/
|
||||
if (domainRegex.test(userSearchInput)) {
|
||||
setSearchUsersLoading(true)
|
||||
|
||||
const pubkey = await handleSearchUserNip05(userSearchInput)
|
||||
|
||||
setSearchUsersLoading(false)
|
||||
|
||||
if (pubkey) {
|
||||
setUserInput(userSearchInput)
|
||||
} else {
|
||||
toast.error(`No user found with the NIP05: ${userSearchInput}`)
|
||||
}
|
||||
} else {
|
||||
handleSearchUsers()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -782,7 +822,7 @@ export const CreatePage = () => {
|
||||
}
|
||||
|
||||
// Send notifications to signers and viewers
|
||||
const sendNotifications = (meta: Meta) => {
|
||||
const sendNotifications = (notification: SigitNotification) => {
|
||||
// no need to send notification to self so remove it from the list
|
||||
const receivers = (
|
||||
signers.length > 0
|
||||
@ -790,7 +830,7 @@ export const CreatePage = () => {
|
||||
: viewers.map((viewer) => viewer.pubkey)
|
||||
).filter((receiver) => receiver !== usersPubkey)
|
||||
|
||||
return receivers.map((receiver) => sendNotification(receiver, meta))
|
||||
return receivers.map((receiver) => sendNotification(receiver, notification))
|
||||
}
|
||||
|
||||
const extractNostrId = (stringifiedEvent: string): string => {
|
||||
@ -865,11 +905,17 @@ export const CreatePage = () => {
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Updating user app data')
|
||||
|
||||
const event = await updateUsersAppData(meta)
|
||||
if (!event) return
|
||||
|
||||
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||
|
||||
setLoadingSpinnerDesc('Sending notifications to counterparties')
|
||||
const promises = sendNotifications(meta)
|
||||
const promises = sendNotifications({
|
||||
metaUrl,
|
||||
keys: meta.keys
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
.then(() => {
|
||||
@ -959,19 +1005,6 @@ export const CreatePage = () => {
|
||||
} else {
|
||||
disarmAddOnEnter()
|
||||
}
|
||||
} else if (value.includes('@')) {
|
||||
// Seems like it's nip05 format
|
||||
const { pubkey } = await queryNip05(value).catch((err) => {
|
||||
console.error(err)
|
||||
return { pubkey: null }
|
||||
})
|
||||
|
||||
if (pubkey) {
|
||||
// Arm the manual user npub add after enter is hit, we don't want to trigger search
|
||||
setPastedUserNpubOrNip05(hexToNpub(pubkey))
|
||||
} else {
|
||||
disarmAddOnEnter()
|
||||
}
|
||||
} else {
|
||||
// Disarm the add user on enter hit, and trigger search after 1 second
|
||||
disarmAddOnEnter()
|
||||
@ -1146,7 +1179,9 @@ export const CreatePage = () => {
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleAddUser}
|
||||
onClick={() => {
|
||||
setUserInput(userSearchInput)
|
||||
}}
|
||||
variant="contained"
|
||||
aria-label="Add"
|
||||
className={styles.counterpartToggleButton}
|
||||
|
@ -1,67 +1,55 @@
|
||||
import { Box, Button, Typography } from '@mui/material'
|
||||
import axios from 'axios'
|
||||
import saveAs from 'file-saver'
|
||||
import JSZip from 'jszip'
|
||||
import _ from 'lodash'
|
||||
import { MuiFileInput } from 'mui-file-input'
|
||||
import { Event, verifyEvent } from 'nostr-tools'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useAppSelector } from '../../hooks/store'
|
||||
import { useAppSelector } from '../../hooks'
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { NostrController } from '../../controllers'
|
||||
import { appPublicRoutes } from '../../routes'
|
||||
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
||||
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
||||
import {
|
||||
ARRAY_BUFFER,
|
||||
decryptArrayBuffer,
|
||||
DEFLATE,
|
||||
encryptArrayBuffer,
|
||||
extractMarksFromSignedMeta,
|
||||
extractZipUrlAndEncryptionKey,
|
||||
filterMarksByPubkey,
|
||||
findOtherUserMarks,
|
||||
generateEncryptionKey,
|
||||
generateKeysFile,
|
||||
getCurrentUserFiles,
|
||||
getCurrentUserMarks,
|
||||
getHash,
|
||||
hexToNpub,
|
||||
isOnline,
|
||||
loadZip,
|
||||
unixNow,
|
||||
npubToHex,
|
||||
parseJson,
|
||||
processMarks,
|
||||
readContentOfZipEntry,
|
||||
sendNotification,
|
||||
signEventForMetaFile,
|
||||
updateUsersAppData,
|
||||
findOtherUserMarks,
|
||||
timeout,
|
||||
processMarks
|
||||
unixNow,
|
||||
updateMarks,
|
||||
updateUsersAppData,
|
||||
uploadMetaToFileStorage
|
||||
} from '../../utils'
|
||||
import { Container } from '../../components/Container'
|
||||
import { DisplayMeta } from './internal/displayMeta'
|
||||
import styles from './style.module.scss'
|
||||
import { CurrentUserMark, Mark } from '../../types/mark.ts'
|
||||
import { getLastSignersSig, isFullySigned } from '../../utils/sign.ts'
|
||||
import {
|
||||
filterMarksByPubkey,
|
||||
getCurrentUserMarks,
|
||||
isCurrentUserMarksComplete,
|
||||
updateMarks
|
||||
} from '../../utils'
|
||||
import PdfMarking from '../../components/PDFView/PdfMarking.tsx'
|
||||
import {
|
||||
convertToSigitFile,
|
||||
getZipWithFiles,
|
||||
SigitFile
|
||||
} from '../../utils/file.ts'
|
||||
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
|
||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
||||
|
||||
enum SignedStatus {
|
||||
Fully_Signed,
|
||||
User_Is_Next_Signer,
|
||||
User_Is_Not_Next_Signer
|
||||
}
|
||||
import { getLastSignersSig } from '../../utils/sign.ts'
|
||||
|
||||
export const SignPage = () => {
|
||||
const navigate = useNavigate()
|
||||
@ -100,17 +88,12 @@ export const SignPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const [displayInput, setDisplayInput] = useState(false)
|
||||
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
|
||||
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
|
||||
const [meta, setMeta] = useState<Meta | null>(null)
|
||||
const [signedStatus, setSignedStatus] = useState<SignedStatus>()
|
||||
|
||||
const [submittedBy, setSubmittedBy] = useState<string>()
|
||||
|
||||
@ -124,66 +107,14 @@ export const SignPage = () => {
|
||||
[key: string]: string | null
|
||||
}>({})
|
||||
|
||||
const [signedBy, setSignedBy] = useState<`npub1${string}`[]>([])
|
||||
|
||||
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 = useAppSelector((state) => state.auth.usersPubkey)
|
||||
|
||||
const nostrController = NostrController.getInstance()
|
||||
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>(
|
||||
[]
|
||||
)
|
||||
const [isMarksCompleted, setIsMarksCompleted] = useState(false)
|
||||
const [otherUserMarks, setOtherUserMarks] = useState<Mark[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (signers.length > 0) {
|
||||
// check if all signers have signed then its fully signed
|
||||
if (isFullySigned(signers, signedBy)) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 handleUpdatedMeta = async (meta: Meta) => {
|
||||
const createSignatureEvent = await parseJson<Event>(
|
||||
@ -263,11 +194,10 @@ export const SignPage = () => {
|
||||
m.value &&
|
||||
encryptionKey
|
||||
) {
|
||||
const decrypted = await fetchAndDecrypt(
|
||||
otherUserMarks[i].value = await fetchAndDecrypt(
|
||||
m.value,
|
||||
encryptionKey
|
||||
)
|
||||
otherUserMarks[i].value = decrypted
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error during mark fetchAndDecrypt phase`, error)
|
||||
@ -278,10 +208,7 @@ export const SignPage = () => {
|
||||
|
||||
setOtherUserMarks(otherUserMarks)
|
||||
setCurrentUserMarks(currentUserMarks)
|
||||
setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks))
|
||||
}
|
||||
|
||||
setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[])
|
||||
}
|
||||
|
||||
if (meta) {
|
||||
@ -290,29 +217,6 @@ export const SignPage = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [meta, usersPubkey])
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
|
||||
setLoadingSpinnerDesc('Generating file')
|
||||
try {
|
||||
const zip = await getZipWithFiles(meta, files)
|
||||
const arrayBuffer = await zip.generateAsync({
|
||||
type: ARRAY_BUFFER,
|
||||
compression: DEFLATE,
|
||||
compressionOptions: {
|
||||
level: 6
|
||||
}
|
||||
})
|
||||
if (!arrayBuffer) return
|
||||
const blob = new Blob([arrayBuffer])
|
||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||
} catch (error) {
|
||||
console.log('error in zip:>> ', error)
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message || 'Error occurred in generating zip file')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const decrypt = useCallback(
|
||||
async (file: File) => {
|
||||
setLoadingSpinnerDesc('Decrypting file')
|
||||
@ -424,7 +328,6 @@ export const SignPage = () => {
|
||||
})
|
||||
} else {
|
||||
setIsLoading(false)
|
||||
setDisplayInput(true)
|
||||
}
|
||||
}, [decryptedArrayBuffer, uploadedZip, metaInNavState, decrypt])
|
||||
|
||||
@ -541,9 +444,6 @@ export const SignPage = () => {
|
||||
|
||||
setFiles(files)
|
||||
setCurrentFileHashes(fileHashes)
|
||||
|
||||
setDisplayInput(false)
|
||||
|
||||
setLoadingSpinnerDesc('Parsing meta.json')
|
||||
|
||||
const metaFileContent = await readContentOfZipEntry(
|
||||
@ -571,21 +471,6 @@ export const SignPage = () => {
|
||||
setMeta(parsedMetaJson)
|
||||
}
|
||||
|
||||
const handleDecrypt = async () => {
|
||||
if (!selectedFile) return
|
||||
|
||||
setIsLoading(true)
|
||||
const arrayBuffer = await decrypt(selectedFile)
|
||||
|
||||
if (!arrayBuffer) {
|
||||
setIsLoading(false)
|
||||
toast.error('Error decrypting file')
|
||||
return
|
||||
}
|
||||
|
||||
handleDecryptedArrayBuffer(arrayBuffer)
|
||||
}
|
||||
|
||||
const handleSign = async () => {
|
||||
if (Object.entries(files).length === 0 || !meta) return
|
||||
|
||||
@ -635,11 +520,18 @@ export const SignPage = () => {
|
||||
}
|
||||
|
||||
if (await isOnline()) {
|
||||
await handleOnlineFlow(updatedMeta)
|
||||
await handleOnlineFlow(updatedMeta, encryptionKey)
|
||||
} else {
|
||||
setMeta(updatedMeta)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
if (metaInNavState) {
|
||||
const createSignature = JSON.parse(metaInNavState.createSignature)
|
||||
navigate(`${appPublicRoutes.verify}/${createSignature.id}`)
|
||||
} else {
|
||||
navigate(appPrivateRoutes.homePage)
|
||||
}
|
||||
}
|
||||
|
||||
// Sign the event for the meta file
|
||||
@ -730,6 +622,14 @@ export const SignPage = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// Check if the current user is the last signer
|
||||
const checkIsLastSigner = (signers: string[]): boolean => {
|
||||
const usersNpub = hexToNpub(usersPubkey!)
|
||||
const lastSignerIndex = signers.length - 1
|
||||
const signerIndex = signers.indexOf(usersNpub)
|
||||
return signerIndex === lastSignerIndex
|
||||
}
|
||||
|
||||
// Handle errors during zip file generation
|
||||
const handleZipError = (err: unknown) => {
|
||||
console.log('Error in zip:>> ', err)
|
||||
@ -741,7 +641,10 @@ export const SignPage = () => {
|
||||
}
|
||||
|
||||
// Handle the online flow: update users app data and send notifications
|
||||
const handleOnlineFlow = async (meta: Meta) => {
|
||||
const handleOnlineFlow = async (
|
||||
meta: Meta,
|
||||
encryptionKey: string | undefined
|
||||
) => {
|
||||
setLoadingSpinnerDesc('Updating users app data')
|
||||
const updatedEvent = await updateUsersAppData(meta)
|
||||
if (!updatedEvent) {
|
||||
@ -749,6 +652,18 @@ export const SignPage = () => {
|
||||
return
|
||||
}
|
||||
|
||||
let metaUrl: string | undefined
|
||||
try {
|
||||
metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message)
|
||||
}
|
||||
console.error(error)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const userSet = new Set<`npub1${string}`>()
|
||||
if (submittedBy && submittedBy !== usersPubkey) {
|
||||
userSet.add(hexToNpub(submittedBy))
|
||||
@ -781,7 +696,7 @@ export const SignPage = () => {
|
||||
setLoadingSpinnerDesc('Sending notifications')
|
||||
const users = Array.from(userSet)
|
||||
const promises = users.map((user) =>
|
||||
sendNotification(npubToHex(user)!, meta)
|
||||
sendNotification(npubToHex(user)!, { metaUrl, keys: meta.keys })
|
||||
)
|
||||
await Promise.all(promises)
|
||||
.then(() => {
|
||||
@ -795,16 +710,38 @@ export const SignPage = () => {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
// Check if the current user is the last signer
|
||||
const checkIsLastSigner = (signers: string[]): boolean => {
|
||||
const usersNpub = hexToNpub(usersPubkey!)
|
||||
const lastSignerIndex = signers.length - 1
|
||||
const signerIndex = signers.indexOf(usersNpub)
|
||||
return signerIndex === lastSignerIndex
|
||||
const handleExport = async () => {
|
||||
const arrayBuffer = await prepareZipExport()
|
||||
if (!arrayBuffer) return
|
||||
|
||||
const blob = new Blob([arrayBuffer])
|
||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
navigate(appPublicRoutes.verify)
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
|
||||
const handleEncryptedExport = async () => {
|
||||
const arrayBuffer = await prepareZipExport()
|
||||
if (!arrayBuffer) return
|
||||
|
||||
const key = await generateEncryptionKey()
|
||||
|
||||
setLoadingSpinnerDesc('Encrypting zip file')
|
||||
const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key)
|
||||
|
||||
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
||||
|
||||
if (!finalZipFile) return
|
||||
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const prepareZipExport = async (): Promise<ArrayBuffer | null> => {
|
||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey)
|
||||
return Promise.resolve(null)
|
||||
|
||||
const usersNpub = hexToNpub(usersPubkey)
|
||||
if (
|
||||
@ -812,15 +749,15 @@ export const SignPage = () => {
|
||||
!viewers.includes(usersNpub) &&
|
||||
submittedBy !== usersNpub
|
||||
)
|
||||
return
|
||||
return Promise.resolve(null)
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Signing nostr event')
|
||||
|
||||
if (!meta) return
|
||||
if (!meta) return Promise.resolve(null)
|
||||
|
||||
const prevSig = getLastSignersSig(meta, signers)
|
||||
if (!prevSig) return
|
||||
if (!prevSig) return Promise.resolve(null)
|
||||
|
||||
const signedEvent = await signEventForMetaFile(
|
||||
JSON.stringify({
|
||||
@ -830,7 +767,7 @@ export const SignPage = () => {
|
||||
setIsLoading
|
||||
)
|
||||
|
||||
if (!signedEvent) return
|
||||
if (!signedEvent) return Promise.resolve(null)
|
||||
|
||||
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
||||
|
||||
@ -848,8 +785,8 @@ export const SignPage = () => {
|
||||
|
||||
const arrayBuffer = await zip
|
||||
.generateAsync({
|
||||
type: 'arraybuffer',
|
||||
compression: 'DEFLATE',
|
||||
type: ARRAY_BUFFER,
|
||||
compression: DEFLATE,
|
||||
compressionOptions: {
|
||||
level: 6
|
||||
}
|
||||
@ -861,50 +798,9 @@ export const SignPage = () => {
|
||||
return null
|
||||
})
|
||||
|
||||
if (!arrayBuffer) return
|
||||
if (!arrayBuffer) return Promise.resolve(null)
|
||||
|
||||
const blob = new Blob([arrayBuffer])
|
||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
navigate(appPublicRoutes.verify)
|
||||
}
|
||||
|
||||
const handleEncryptedExport = async () => {
|
||||
if (Object.entries(files).length === 0 || !meta) return
|
||||
|
||||
const stringifiedMeta = JSON.stringify(meta, null, 2)
|
||||
const zip = await getZipWithFiles(meta, files)
|
||||
|
||||
zip.file('meta.json', stringifiedMeta)
|
||||
|
||||
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 finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
||||
|
||||
if (!finalZipFile) return
|
||||
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
||||
return Promise.resolve(arrayBuffer)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -944,90 +840,17 @@ export const SignPage = () => {
|
||||
return <LoadingSpinner desc={loadingSpinnerDesc} />
|
||||
}
|
||||
|
||||
if (!isMarksCompleted && signedStatus === SignedStatus.User_Is_Next_Signer) {
|
||||
return (
|
||||
<PdfMarking
|
||||
files={getCurrentUserFiles(files, currentFileHashes, creatorFileHashes)}
|
||||
currentUserMarks={currentUserMarks}
|
||||
setIsMarksCompleted={setIsMarksCompleted}
|
||||
setCurrentUserMarks={setCurrentUserMarks}
|
||||
setUpdatedMarks={setUpdatedMarks}
|
||||
handleDownload={handleDownload}
|
||||
otherUserMarks={otherUserMarks}
|
||||
meta={meta}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container className={styles.container}>
|
||||
{displayInput && (
|
||||
<>
|
||||
<Typography component="label" variant="h6">
|
||||
Select sigit file
|
||||
</Typography>
|
||||
|
||||
<Box className={styles.inputBlock}>
|
||||
<MuiFileInput
|
||||
placeholder="Select file"
|
||||
inputProps={{ accept: '.sigit.zip' }}
|
||||
value={selectedFile}
|
||||
onChange={(value) => setSelectedFile(value)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{selectedFile && (
|
||||
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleDecrypt} variant="contained">
|
||||
Decrypt
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{submittedBy && Object.entries(files).length > 0 && meta && (
|
||||
<>
|
||||
<DisplayMeta
|
||||
meta={meta}
|
||||
files={files}
|
||||
submittedBy={submittedBy}
|
||||
signers={signers}
|
||||
viewers={viewers}
|
||||
creatorFileHashes={creatorFileHashes}
|
||||
currentFileHashes={currentFileHashes}
|
||||
signedBy={signedBy}
|
||||
nextSigner={nextSinger}
|
||||
getPrevSignersSig={getPrevSignersSig}
|
||||
/>
|
||||
|
||||
{signedStatus === SignedStatus.Fully_Signed && (
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleExport} variant="contained">
|
||||
Export Sigit
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{signedStatus === SignedStatus.User_Is_Next_Signer && (
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleSign} variant="contained">
|
||||
Sign
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isSignerOrCreator && (
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleEncryptedExport} variant="contained">
|
||||
Export Encrypted Sigit
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
<PdfMarking
|
||||
files={getCurrentUserFiles(files, currentFileHashes, creatorFileHashes)}
|
||||
currentUserMarks={currentUserMarks}
|
||||
setCurrentUserMarks={setCurrentUserMarks}
|
||||
setUpdatedMarks={setUpdatedMarks}
|
||||
handleSign={handleSign}
|
||||
handleExport={handleExport}
|
||||
handleEncryptedExport={handleEncryptedExport}
|
||||
otherUserMarks={otherUserMarks}
|
||||
meta={meta}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -23,7 +23,13 @@ import {
|
||||
getCurrentUserFiles,
|
||||
updateUsersAppData,
|
||||
npubToHex,
|
||||
sendNotification
|
||||
sendNotification,
|
||||
generateEncryptionKey,
|
||||
encryptArrayBuffer,
|
||||
generateKeysFile,
|
||||
ARRAY_BUFFER,
|
||||
DEFLATE,
|
||||
uploadMetaToFileStorage
|
||||
} from '../../utils'
|
||||
import styles from './style.module.scss'
|
||||
import { useLocation, useParams } from 'react-router-dom'
|
||||
@ -351,6 +357,11 @@ export const VerifyPage = () => {
|
||||
const updatedEvent = await updateUsersAppData(updatedMeta)
|
||||
if (!updatedEvent) return
|
||||
|
||||
const metaUrl = await uploadMetaToFileStorage(
|
||||
updatedMeta,
|
||||
encryptionKey
|
||||
)
|
||||
|
||||
const userSet = new Set<`npub1${string}`>()
|
||||
signers.forEach((signer) => {
|
||||
if (signer !== usersPubkey) {
|
||||
@ -364,7 +375,10 @@ export const VerifyPage = () => {
|
||||
|
||||
const users = Array.from(userSet)
|
||||
const promises = users.map((user) =>
|
||||
sendNotification(npubToHex(user)!, updatedMeta)
|
||||
sendNotification(npubToHex(user)!, {
|
||||
metaUrl,
|
||||
keys: meta.keys!
|
||||
})
|
||||
)
|
||||
|
||||
await Promise.all(promises)
|
||||
@ -541,8 +555,114 @@ export const VerifyPage = () => {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const handleMarkedExport = async () => {
|
||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
|
||||
// Handle errors during zip file generation
|
||||
const handleZipError = (err: unknown) => {
|
||||
console.log('Error in zip:>> ', err)
|
||||
setIsLoading(false)
|
||||
if (err instanceof Error) {
|
||||
toast.error(err.message || 'Error occurred in generating zip file')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if the current user is the last signer
|
||||
const checkIsLastSigner = (signers: string[]): boolean => {
|
||||
const usersNpub = hexToNpub(usersPubkey!)
|
||||
const lastSignerIndex = signers.length - 1
|
||||
const signerIndex = signers.indexOf(usersNpub)
|
||||
return signerIndex === lastSignerIndex
|
||||
}
|
||||
|
||||
// create final zip file
|
||||
const createFinalZipFile = async (
|
||||
encryptedArrayBuffer: ArrayBuffer,
|
||||
encryptionKey: string
|
||||
): Promise<File | null> => {
|
||||
// Get the current timestamp in seconds
|
||||
const blob = new Blob([encryptedArrayBuffer])
|
||||
// Create a File object with the Blob data
|
||||
const file = new File([blob], `compressed.sigit`, {
|
||||
type: 'application/sigit'
|
||||
})
|
||||
|
||||
const isLastSigner = checkIsLastSigner(signers)
|
||||
|
||||
const userSet = new Set<string>()
|
||||
|
||||
if (isLastSigner) {
|
||||
if (submittedBy) {
|
||||
userSet.add(submittedBy)
|
||||
}
|
||||
|
||||
signers.forEach((signer) => {
|
||||
userSet.add(npubToHex(signer)!)
|
||||
})
|
||||
|
||||
viewers.forEach((viewer) => {
|
||||
userSet.add(npubToHex(viewer)!)
|
||||
})
|
||||
} else {
|
||||
const usersNpub = hexToNpub(usersPubkey!)
|
||||
const signerIndex = signers.indexOf(usersNpub)
|
||||
const nextSigner = signers[signerIndex + 1]
|
||||
userSet.add(npubToHex(nextSigner)!)
|
||||
}
|
||||
|
||||
const keysFileContent = await generateKeysFile(
|
||||
Array.from(userSet),
|
||||
encryptionKey
|
||||
)
|
||||
if (!keysFileContent) return null
|
||||
|
||||
const zip = new JSZip()
|
||||
zip.file(`compressed.sigit`, file)
|
||||
zip.file('keys.json', keysFileContent)
|
||||
|
||||
const arraybuffer = await zip
|
||||
.generateAsync({
|
||||
type: 'arraybuffer',
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: { level: 6 }
|
||||
})
|
||||
.catch(handleZipError)
|
||||
|
||||
if (!arraybuffer) return null
|
||||
|
||||
return new File([new Blob([arraybuffer])], `${unixNow()}.sigit.zip`, {
|
||||
type: 'application/zip'
|
||||
})
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
const arrayBuffer = await prepareZipExport()
|
||||
if (!arrayBuffer) return
|
||||
|
||||
const blob = new Blob([arrayBuffer])
|
||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const handleEncryptedExport = async () => {
|
||||
const arrayBuffer = await prepareZipExport()
|
||||
if (!arrayBuffer) return
|
||||
|
||||
const key = await generateEncryptionKey()
|
||||
|
||||
setLoadingSpinnerDesc('Encrypting zip file')
|
||||
const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key)
|
||||
|
||||
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
||||
|
||||
if (!finalZipFile) return
|
||||
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const prepareZipExport = async (): Promise<ArrayBuffer | null> => {
|
||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey)
|
||||
return Promise.resolve(null)
|
||||
|
||||
const usersNpub = hexToNpub(usersPubkey)
|
||||
if (
|
||||
@ -550,14 +670,14 @@ export const VerifyPage = () => {
|
||||
!viewers.includes(usersNpub) &&
|
||||
submittedBy !== usersNpub
|
||||
) {
|
||||
return
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Signing nostr event')
|
||||
|
||||
const prevSig = getLastSignersSig(meta, signers)
|
||||
if (!prevSig) return
|
||||
if (!prevSig) return Promise.resolve(null)
|
||||
|
||||
const signedEvent = await signEventForMetaFile(
|
||||
JSON.stringify({ prevSig }),
|
||||
@ -565,7 +685,7 @@ export const VerifyPage = () => {
|
||||
setIsLoading
|
||||
)
|
||||
|
||||
if (!signedEvent) return
|
||||
if (!signedEvent) return Promise.resolve(null)
|
||||
|
||||
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
||||
const updatedMeta = { ...meta, exportSignature }
|
||||
@ -576,8 +696,8 @@ export const VerifyPage = () => {
|
||||
|
||||
const arrayBuffer = await zip
|
||||
.generateAsync({
|
||||
type: 'arraybuffer',
|
||||
compression: 'DEFLATE',
|
||||
type: ARRAY_BUFFER,
|
||||
compression: DEFLATE,
|
||||
compressionOptions: {
|
||||
level: 6
|
||||
}
|
||||
@ -589,12 +709,9 @@ export const VerifyPage = () => {
|
||||
return null
|
||||
})
|
||||
|
||||
if (!arrayBuffer) return
|
||||
if (!arrayBuffer) return Promise.resolve(null)
|
||||
|
||||
const blob = new Blob([arrayBuffer])
|
||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||
|
||||
setIsLoading(false)
|
||||
return Promise.resolve(arrayBuffer)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -640,8 +757,8 @@ export const VerifyPage = () => {
|
||||
)}
|
||||
currentFile={currentFile}
|
||||
setCurrentFile={setCurrentFile}
|
||||
handleDownload={handleMarkedExport}
|
||||
downloadLabel="Download Sigit"
|
||||
handleExport={handleExport}
|
||||
handleEncryptedExport={handleEncryptedExport}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -84,3 +84,12 @@ export interface UserAppData {
|
||||
export interface DocSignatureEvent extends Event {
|
||||
parsedContent?: SignedEventContent
|
||||
}
|
||||
|
||||
export interface SigitNotification {
|
||||
metaUrl: string
|
||||
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
|
||||
}
|
||||
|
||||
export function isSigitNotification(obj: unknown): obj is SigitNotification {
|
||||
return typeof (obj as SigitNotification).metaUrl === 'string'
|
||||
}
|
||||
|
26
src/types/errors/MetaStorageError.ts
Normal file
26
src/types/errors/MetaStorageError.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Jsonable } from '.'
|
||||
|
||||
export enum MetaStorageErrorType {
|
||||
'ENCRYPTION_KEY_REQUIRED' = 'Encryption key is required.',
|
||||
'HASHING_FAILED' = "Can't get encrypted file hash.",
|
||||
'FETCH_FAILED' = 'Fetching meta.json requires an encryption key.',
|
||||
'HASH_VERIFICATION_FAILED' = 'Unable to verify meta.json.',
|
||||
'DECRYPTION_FAILED' = 'Error decryping meta.json.',
|
||||
'UNHANDLED_FETCH_ERROR' = 'Unable to fetch meta.json. Something went wrong.'
|
||||
}
|
||||
|
||||
export class MetaStorageError extends Error {
|
||||
public readonly context?: Jsonable
|
||||
|
||||
constructor(
|
||||
message: MetaStorageErrorType,
|
||||
options: { cause?: Error; context?: Jsonable } = {}
|
||||
) {
|
||||
const { cause, context } = options
|
||||
|
||||
super(message, { cause })
|
||||
this.name = this.constructor.name
|
||||
|
||||
this.context = context
|
||||
}
|
||||
}
|
@ -120,5 +120,5 @@ export const SIGNATURE_PAD_OPTIONS = {
|
||||
|
||||
export const SIGNATURE_PAD_SIZE = {
|
||||
width: 300,
|
||||
height: 300
|
||||
height: 150
|
||||
}
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { CreateSignatureEventContent, Meta } from '../types'
|
||||
import { fromUnixTimestamp, parseJson } from '.'
|
||||
import {
|
||||
decryptArrayBuffer,
|
||||
encryptArrayBuffer,
|
||||
fromUnixTimestamp,
|
||||
getHash,
|
||||
parseJson,
|
||||
uploadToFileStorage
|
||||
} from '.'
|
||||
import { Event, verifyEvent } from 'nostr-tools'
|
||||
import { toast } from 'react-toastify'
|
||||
import { extractFileExtensions } from './file'
|
||||
@ -8,6 +15,11 @@ import {
|
||||
MetaParseError,
|
||||
MetaParseErrorType
|
||||
} from '../types/errors/MetaParseError'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
MetaStorageError,
|
||||
MetaStorageErrorType
|
||||
} from '../types/errors/MetaStorageError'
|
||||
|
||||
export enum SignStatus {
|
||||
Signed = 'Signed',
|
||||
@ -126,3 +138,76 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const uploadMetaToFileStorage = async (
|
||||
meta: Meta,
|
||||
encryptionKey: string | undefined
|
||||
) => {
|
||||
// Value is the stringified meta object
|
||||
const value = JSON.stringify(meta)
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
// Encode it to the arrayBuffer
|
||||
const uint8Array = encoder.encode(value)
|
||||
|
||||
if (!encryptionKey) {
|
||||
throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED)
|
||||
}
|
||||
|
||||
// Encrypt the file contents with the same encryption key from the create signature
|
||||
const encryptedArrayBuffer = await encryptArrayBuffer(
|
||||
uint8Array,
|
||||
encryptionKey
|
||||
)
|
||||
|
||||
const hash = await getHash(encryptedArrayBuffer)
|
||||
if (!hash) {
|
||||
throw new MetaStorageError(MetaStorageErrorType.HASHING_FAILED)
|
||||
}
|
||||
|
||||
// Create the encrypted json file from array buffer and hash
|
||||
const file = new File([encryptedArrayBuffer], `${hash}.json`)
|
||||
const url = await uploadToFileStorage(file)
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export const fetchMetaFromFileStorage = async (
|
||||
url: string,
|
||||
encryptionKey: string | undefined
|
||||
) => {
|
||||
if (!encryptionKey) {
|
||||
throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED)
|
||||
}
|
||||
|
||||
const encryptedArrayBuffer = await axios.get(url, {
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
|
||||
// Verify hash
|
||||
const parts = url.split('/')
|
||||
const urlHash = parts[parts.length - 1]
|
||||
const hash = await getHash(encryptedArrayBuffer.data)
|
||||
if (hash !== urlHash) {
|
||||
throw new MetaStorageError(MetaStorageErrorType.HASH_VERIFICATION_FAILED)
|
||||
}
|
||||
|
||||
const arrayBuffer = await decryptArrayBuffer(
|
||||
encryptedArrayBuffer.data,
|
||||
encryptionKey
|
||||
).catch((err) => {
|
||||
throw new MetaStorageError(MetaStorageErrorType.DECRYPTION_FAILED, {
|
||||
cause: err
|
||||
})
|
||||
})
|
||||
|
||||
if (arrayBuffer) {
|
||||
// Decode meta.json and parse
|
||||
const decoder = new TextDecoder()
|
||||
const json = decoder.decode(arrayBuffer)
|
||||
const meta = await parseJson<Meta>(json)
|
||||
return meta
|
||||
}
|
||||
|
||||
throw new MetaStorageError(MetaStorageErrorType.UNHANDLED_FETCH_ERROR)
|
||||
}
|
||||
|
@ -29,12 +29,20 @@ import {
|
||||
} from '../store/actions'
|
||||
import { Keys } from '../store/auth/types'
|
||||
import store from '../store/store'
|
||||
import { Meta, ProfileMetadata, SignedEvent, UserAppData } from '../types'
|
||||
import {
|
||||
isSigitNotification,
|
||||
Meta,
|
||||
ProfileMetadata,
|
||||
SigitNotification,
|
||||
SignedEvent,
|
||||
UserAppData
|
||||
} from '../types'
|
||||
import { getDefaultRelayMap } from './relays'
|
||||
import { parseJson, removeLeadingSlash } from './string'
|
||||
import { timeout } from './utils'
|
||||
import { getHash } from './hash'
|
||||
import { SIGIT_BLOSSOM } from './const.ts'
|
||||
import { fetchMetaFromFileStorage } from './meta.ts'
|
||||
|
||||
/**
|
||||
* Generates a `d` tag for userAppData
|
||||
@ -908,17 +916,48 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
|
||||
|
||||
if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938) return
|
||||
|
||||
const meta = await parseJson<Meta>(internalUnsignedEvent.content).catch(
|
||||
(err) => {
|
||||
console.log(
|
||||
'An error occurred in parsing the internal unsigned event',
|
||||
err
|
||||
)
|
||||
return null
|
||||
}
|
||||
)
|
||||
const parsedContent = await parseJson<Meta | SigitNotification>(
|
||||
internalUnsignedEvent.content
|
||||
).catch((err) => {
|
||||
console.log('An error occurred in parsing the internal unsigned event', err)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!meta) return
|
||||
if (!parsedContent) return
|
||||
let meta: Meta
|
||||
if (isSigitNotification(parsedContent)) {
|
||||
const notification = parsedContent
|
||||
let encryptionKey: string | undefined
|
||||
if (!notification.keys) return
|
||||
|
||||
const { sender, keys } = notification.keys
|
||||
|
||||
// Retrieve the user's public key from the state
|
||||
const usersPubkey = store.getState().auth.usersPubkey!
|
||||
const usersNpub = hexToNpub(usersPubkey)
|
||||
|
||||
// Check if the user's public key is in the keys object
|
||||
if (usersNpub in keys) {
|
||||
// Instantiate the NostrController to decrypt the encryption key
|
||||
const nostrController = NostrController.getInstance()
|
||||
const decrypted = await nostrController
|
||||
.nip04Decrypt(sender, keys[usersNpub])
|
||||
.catch((err) => {
|
||||
console.log('An error occurred in decrypting encryption key', err)
|
||||
return undefined
|
||||
})
|
||||
|
||||
encryptionKey = decrypted
|
||||
}
|
||||
try {
|
||||
meta = await fetchMetaFromFileStorage(notification.metaUrl, encryptionKey)
|
||||
} catch (error) {
|
||||
console.error(`An error occured fetching meta file from storage`, error)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
meta = parsedContent
|
||||
}
|
||||
|
||||
await updateUsersAppData(meta)
|
||||
}
|
||||
@ -926,9 +965,12 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
|
||||
/**
|
||||
* Function to send a notification to a specified receiver.
|
||||
* @param receiver - The recipient's public key.
|
||||
* @param meta - Metadata associated with the notification.
|
||||
* @param notification - Url pointing to metadata associated with the notification on blossom and keys to decrypt.
|
||||
*/
|
||||
export const sendNotification = async (receiver: string, meta: Meta) => {
|
||||
export const sendNotification = async (
|
||||
receiver: string,
|
||||
notification: SigitNotification
|
||||
) => {
|
||||
// Retrieve the user's public key from the state
|
||||
const usersPubkey = store.getState().auth.usersPubkey!
|
||||
|
||||
@ -936,7 +978,7 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
kind: 938,
|
||||
pubkey: usersPubkey,
|
||||
content: JSON.stringify(meta),
|
||||
content: JSON.stringify(notification),
|
||||
tags: [],
|
||||
created_at: unixNow()
|
||||
}
|
||||
|
@ -80,8 +80,8 @@ const getUserRelaySet = (tags: string[][]): RelaySet => {
|
||||
}
|
||||
|
||||
const getDefaultRelaySet = (): RelaySet => ({
|
||||
read: [SIGIT_RELAY],
|
||||
write: [SIGIT_RELAY]
|
||||
read: DEFAULT_LOOK_UP_RELAY_LIST,
|
||||
write: DEFAULT_LOOK_UP_RELAY_LIST
|
||||
})
|
||||
|
||||
const getDefaultRelayMap = (): RelayMap => ({
|
||||
|
Loading…
Reference in New Issue
Block a user