2024-09-05 13:24:34 +02:00
|
|
|
import styles from './style.module.scss'
|
2024-11-19 12:03:41 +01:00
|
|
|
import {
|
|
|
|
Box,
|
|
|
|
Button,
|
|
|
|
CircularProgress,
|
|
|
|
FormHelperText,
|
|
|
|
TextField,
|
|
|
|
Tooltip
|
|
|
|
} from '@mui/material'
|
2024-06-28 14:24:14 +05:00
|
|
|
import type { Identifier, XYCoord } from 'dnd-core'
|
|
|
|
import saveAs from 'file-saver'
|
2024-05-16 16:22:05 +05:00
|
|
|
import JSZip from 'jszip'
|
2024-06-28 14:24:14 +05:00
|
|
|
import { Event, kinds } from 'nostr-tools'
|
2024-11-19 12:03:41 +01:00
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
2024-09-05 13:24:34 +02:00
|
|
|
import { DndProvider, useDrag, useDrop } from 'react-dnd'
|
|
|
|
import { MultiBackend } from 'react-dnd-multi-backend'
|
2024-09-05 09:28:04 +02:00
|
|
|
import { HTML5toTouch } from 'rdndmb-html5-to-touch'
|
2024-10-08 17:08:43 +02:00
|
|
|
import { useAppSelector } from '../../hooks/store'
|
2024-06-13 11:47:28 +05:00
|
|
|
import { useLocation, useNavigate } from 'react-router-dom'
|
2024-05-16 16:22:05 +05:00
|
|
|
import { toast } from 'react-toastify'
|
2024-04-18 16:12:11 +05:00
|
|
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
2024-07-31 13:53:36 +02:00
|
|
|
import { UserAvatar } from '../../components/UserAvatar'
|
2024-11-19 12:03:41 +01:00
|
|
|
import {
|
|
|
|
MetadataController,
|
|
|
|
NostrController,
|
|
|
|
RelayController
|
|
|
|
} from '../../controllers'
|
2024-12-24 10:58:29 +01:00
|
|
|
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
2024-07-05 13:38:04 +05:00
|
|
|
import {
|
|
|
|
CreateSignatureEventContent,
|
2024-11-21 09:20:20 +01:00
|
|
|
KeyboardCode,
|
2024-07-05 13:38:04 +05:00
|
|
|
Meta,
|
|
|
|
ProfileMetadata,
|
2024-12-06 20:00:38 +01:00
|
|
|
SigitNotification,
|
2024-09-27 14:18:26 +03:00
|
|
|
SignedEvent,
|
2024-07-05 13:38:04 +05:00
|
|
|
User,
|
2024-11-21 09:20:20 +01:00
|
|
|
UserRole
|
2024-07-05 13:38:04 +05:00
|
|
|
} from '../../types'
|
2024-04-18 16:12:11 +05:00
|
|
|
import {
|
|
|
|
encryptArrayBuffer,
|
2024-07-09 01:16:47 +05:00
|
|
|
formatTimestamp,
|
2024-04-18 16:12:11 +05:00
|
|
|
generateEncryptionKey,
|
2024-06-28 14:24:14 +05:00
|
|
|
generateKeys,
|
2024-06-12 15:02:26 +05:00
|
|
|
generateKeysFile,
|
2024-04-18 16:12:11 +05:00
|
|
|
getHash,
|
2024-05-14 14:27:05 +05:00
|
|
|
hexToNpub,
|
2024-05-31 12:14:33 +05:00
|
|
|
isOnline,
|
2024-08-13 11:52:05 +02:00
|
|
|
unixNow,
|
2024-05-17 13:34:56 +05:00
|
|
|
npubToHex,
|
2024-05-14 14:27:05 +05:00
|
|
|
queryNip05,
|
2024-06-28 14:24:14 +05:00
|
|
|
sendNotification,
|
2024-04-18 16:12:11 +05:00
|
|
|
signEventForMetaFile,
|
2024-06-28 14:24:14 +05:00
|
|
|
updateUsersAppData,
|
2024-09-17 14:33:50 +02:00
|
|
|
uploadToFileStorage,
|
2024-09-20 10:26:32 +02:00
|
|
|
DEFAULT_TOOLBOX,
|
2024-12-06 20:00:38 +01:00
|
|
|
settleAllFullfilfedPromises,
|
2024-12-18 12:40:01 +01:00
|
|
|
DEFAULT_LOOK_UP_RELAY_LIST,
|
2024-12-06 20:00:38 +01:00
|
|
|
uploadMetaToFileStorage
|
2024-04-18 16:12:11 +05:00
|
|
|
} from '../../utils'
|
2024-08-06 12:47:48 +03:00
|
|
|
import { Container } from '../../components/Container'
|
2024-08-19 14:55:26 +02:00
|
|
|
import fileListStyles from '../../components/FileList/style.module.scss'
|
2024-09-17 14:33:50 +02:00
|
|
|
import { DrawTool } from '../../types/drawing'
|
2024-07-12 09:15:52 +02:00
|
|
|
import { DrawPDFFields } from '../../components/DrawPDFFields'
|
2024-07-18 15:38:07 +03:00
|
|
|
import { Mark } from '../../types/mark.ts'
|
2024-08-15 18:57:40 +02:00
|
|
|
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
2024-08-19 08:30:22 +02:00
|
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
|
|
import {
|
2024-08-19 18:05:14 +02:00
|
|
|
faEllipsis,
|
2024-08-19 08:30:22 +02:00
|
|
|
faEye,
|
2024-09-04 14:05:36 +02:00
|
|
|
faFile,
|
|
|
|
faFileCirclePlus,
|
2024-08-20 11:46:24 +02:00
|
|
|
faGripLines,
|
2024-08-19 08:30:22 +02:00
|
|
|
faPen,
|
|
|
|
faPlus,
|
2024-11-19 12:03:41 +01:00
|
|
|
faSearch,
|
2024-09-04 14:05:36 +02:00
|
|
|
faToolbox,
|
2024-08-19 08:30:22 +02:00
|
|
|
faTrash,
|
|
|
|
faUpload
|
|
|
|
} from '@fortawesome/free-solid-svg-icons'
|
2024-09-20 10:26:32 +02:00
|
|
|
import { getSigitFile, SigitFile } from '../../utils/file.ts'
|
2024-09-27 14:18:26 +03:00
|
|
|
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
2024-11-19 12:03:41 +01:00
|
|
|
import { Autocomplete } from '@mui/lab'
|
|
|
|
import _, { truncate } from 'lodash'
|
|
|
|
import * as React from 'react'
|
2024-11-21 09:20:20 +01:00
|
|
|
import { AvatarIconButton } from '../../components/UserAvatarIconButton'
|
2024-11-19 12:03:41 +01:00
|
|
|
|
2024-11-21 09:20:20 +01:00
|
|
|
type FoundUser = Event & { npub: string }
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-05-15 13:41:55 +05:00
|
|
|
export const CreatePage = () => {
|
|
|
|
const navigate = useNavigate()
|
2024-06-13 11:47:28 +05:00
|
|
|
const location = useLocation()
|
2024-08-08 17:30:49 +02:00
|
|
|
const { uploadedFiles } = location.state || {}
|
2024-08-19 14:55:26 +02:00
|
|
|
const [currentFile, setCurrentFile] = useState<File>()
|
|
|
|
const isActive = (file: File) => file.name === currentFile?.name
|
2024-06-13 11:47:28 +05:00
|
|
|
|
2024-04-18 16:12:11 +05:00
|
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
|
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
2024-05-14 14:27:05 +05:00
|
|
|
|
2024-07-09 01:16:47 +05:00
|
|
|
const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
|
2024-08-19 08:30:22 +02:00
|
|
|
|
2024-05-14 14:27:05 +05:00
|
|
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
2024-08-19 08:30:22 +02:00
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
|
|
const handleUploadButtonClick = () => {
|
|
|
|
if (fileInputRef.current) {
|
|
|
|
fileInputRef.current.click()
|
|
|
|
}
|
|
|
|
}
|
2024-05-14 14:27:05 +05:00
|
|
|
|
|
|
|
const [userInput, setUserInput] = useState('')
|
2024-11-19 12:03:41 +01:00
|
|
|
const [userSearchInput, setUserSearchInput] = useState('')
|
|
|
|
|
2024-11-21 11:06:31 +01:00
|
|
|
const [userRole] = useState<UserRole>(UserRole.signer)
|
2024-05-14 14:27:05 +05:00
|
|
|
const [error, setError] = useState<string>()
|
|
|
|
|
|
|
|
const [users, setUsers] = useState<User[]>([])
|
2024-09-13 18:13:34 +02:00
|
|
|
const signers = users.filter((u) => u.role === UserRole.signer)
|
|
|
|
const viewers = users.filter((u) => u.role === UserRole.viewer)
|
2024-05-14 14:27:05 +05:00
|
|
|
|
2024-11-19 12:03:41 +01:00
|
|
|
const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)!
|
2024-04-18 16:12:11 +05:00
|
|
|
|
|
|
|
const nostrController = NostrController.getInstance()
|
|
|
|
|
2024-07-11 16:16:36 +02:00
|
|
|
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
|
|
|
{}
|
|
|
|
)
|
2024-08-22 18:20:54 +02:00
|
|
|
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
|
2024-09-20 10:26:32 +02:00
|
|
|
const [parsingPdf, setIsParsing] = useState<boolean>(false)
|
2024-11-19 12:03:41 +01:00
|
|
|
|
2024-11-21 09:20:20 +01:00
|
|
|
const searchFieldRef = useRef<HTMLInputElement>(null)
|
|
|
|
|
2024-11-19 12:03:41 +01:00
|
|
|
const [selectedTool, setSelectedTool] = useState<DrawTool>()
|
|
|
|
|
2024-11-21 09:20:20 +01:00
|
|
|
const [foundUsers, setFoundUsers] = useState<FoundUser[]>([])
|
2024-11-19 12:03:41 +01:00
|
|
|
const [searchUsersLoading, setSearchUsersLoading] = useState<boolean>(false)
|
2024-11-21 09:20:20 +01:00
|
|
|
const [pastedUserNpubOrNip05, setPastedUserNpubOrNip05] = useState<
|
|
|
|
string | undefined
|
|
|
|
>()
|
2024-11-19 12:03:41 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Fired when user select
|
|
|
|
*/
|
|
|
|
const handleSearchUserChange = useCallback(
|
2024-11-21 09:20:20 +01:00
|
|
|
(_event: React.SyntheticEvent, value: string | FoundUser | null) => {
|
2024-11-19 12:03:41 +01:00
|
|
|
if (typeof value === 'object') {
|
2024-11-21 09:20:20 +01:00
|
|
|
const ndkEvent = value as FoundUser
|
|
|
|
if (ndkEvent?.pubkey) {
|
|
|
|
setUserInput(hexToNpub(ndkEvent.pubkey))
|
|
|
|
}
|
2024-11-19 12:03:41 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
[setUserInput]
|
|
|
|
)
|
|
|
|
|
2024-12-06 16:22:23 +01:00
|
|
|
const handleSearchUserNip05 = async (
|
|
|
|
nip05: string
|
|
|
|
): Promise<string | null> => {
|
|
|
|
const { pubkey } = await queryNip05(nip05).catch((err) => {
|
|
|
|
console.error(err)
|
|
|
|
return { pubkey: null }
|
|
|
|
})
|
|
|
|
|
|
|
|
return pubkey
|
|
|
|
}
|
|
|
|
|
2024-11-19 12:03:41 +01:00
|
|
|
const handleSearchUsers = async (searchValue?: string) => {
|
|
|
|
const searchString = searchValue || userSearchInput || undefined
|
|
|
|
|
|
|
|
if (!searchString) return
|
|
|
|
|
|
|
|
setSearchUsersLoading(true)
|
|
|
|
|
|
|
|
const relayController = RelayController.getInstance()
|
|
|
|
const metadataController = MetadataController.getInstance()
|
|
|
|
|
|
|
|
const relaySet = await metadataController.findRelayListMetadata(usersPubkey)
|
2024-12-02 11:51:02 +01:00
|
|
|
|
|
|
|
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)]
|
|
|
|
|
2024-11-19 12:03:41 +01:00
|
|
|
const searchTerm = searchString.trim()
|
|
|
|
|
|
|
|
relayController
|
|
|
|
.fetchEvents(
|
|
|
|
{
|
|
|
|
kinds: [0],
|
|
|
|
search: searchTerm
|
|
|
|
},
|
2024-12-02 11:51:02 +01:00
|
|
|
uniqueReadRelaySet
|
2024-11-19 12:03:41 +01:00
|
|
|
)
|
|
|
|
.then((events) => {
|
2024-11-21 09:20:20 +01:00
|
|
|
console.log('events', events)
|
|
|
|
|
|
|
|
const fineFilteredEvents: FoundUser[] = events
|
|
|
|
.filter((event) => {
|
|
|
|
const lowercaseContent = event.content.toLowerCase()
|
|
|
|
|
|
|
|
return (
|
|
|
|
lowercaseContent.includes(
|
|
|
|
`"name":"${searchTerm.toLowerCase()}"`
|
|
|
|
) ||
|
|
|
|
lowercaseContent.includes(
|
|
|
|
`"display_name":"${searchTerm.toLowerCase()}"`
|
|
|
|
) ||
|
|
|
|
lowercaseContent.includes(
|
|
|
|
`"username":"${searchTerm.toLowerCase()}"`
|
|
|
|
) ||
|
|
|
|
lowercaseContent.includes(`"nip05":"${searchTerm.toLowerCase()}"`)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.reduce((uniqueEvents: FoundUser[], event: Event) => {
|
|
|
|
if (!uniqueEvents.some((e: Event) => e.pubkey === event.pubkey)) {
|
|
|
|
uniqueEvents.push({
|
|
|
|
...event,
|
|
|
|
npub: hexToNpub(event.pubkey)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return uniqueEvents
|
|
|
|
}, [])
|
|
|
|
|
2024-12-02 15:34:17 +01:00
|
|
|
console.info('fineFilteredEvents', fineFilteredEvents)
|
2024-11-19 12:03:41 +01:00
|
|
|
setFoundUsers(fineFilteredEvents)
|
2024-12-02 15:34:17 +01:00
|
|
|
|
|
|
|
if (!fineFilteredEvents.length)
|
|
|
|
toast.info('No user found with the provided search term')
|
2024-11-19 12:03:41 +01:00
|
|
|
})
|
|
|
|
.catch((error) => {
|
|
|
|
console.error(error)
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
setSearchUsersLoading(false)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-11-21 09:20:20 +01:00
|
|
|
useEffect(() => {
|
|
|
|
setTimeout(() => {
|
|
|
|
if (foundUsers.length) {
|
|
|
|
if (searchFieldRef.current) {
|
|
|
|
searchFieldRef.current.blur()
|
|
|
|
searchFieldRef.current.focus()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}, [foundUsers])
|
2024-11-19 12:03:41 +01:00
|
|
|
|
2024-12-06 16:22:23 +01:00
|
|
|
const handleInputKeyDown = async (
|
|
|
|
event: React.KeyboardEvent<HTMLDivElement>
|
|
|
|
) => {
|
2024-11-19 12:03:41 +01:00
|
|
|
if (
|
|
|
|
event.code === KeyboardCode.Enter ||
|
|
|
|
event.code === KeyboardCode.NumpadEnter
|
|
|
|
) {
|
|
|
|
event.preventDefault()
|
2024-11-21 09:20:20 +01:00
|
|
|
|
|
|
|
// If pasted user npub of nip05 is present, we just add the user to the counterparts list
|
|
|
|
if (pastedUserNpubOrNip05) {
|
|
|
|
setUserInput(pastedUserNpubOrNip05)
|
|
|
|
setPastedUserNpubOrNip05(undefined)
|
|
|
|
} else {
|
|
|
|
// Otherwize if search already provided some results, user must manually click the search button
|
|
|
|
if (!foundUsers.length) {
|
2024-12-06 20:58:20 +01:00
|
|
|
// 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)
|
|
|
|
|
2024-12-06 16:22:23 +01:00
|
|
|
const pubkey = await handleSearchUserNip05(userSearchInput)
|
|
|
|
|
2024-12-06 20:58:20 +01:00
|
|
|
setSearchUsersLoading(false)
|
|
|
|
|
2024-12-06 16:22:23 +01:00
|
|
|
if (pubkey) {
|
|
|
|
setUserInput(userSearchInput)
|
|
|
|
} else {
|
|
|
|
toast.error(`No user found with the NIP05: ${userSearchInput}`)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
handleSearchUsers()
|
|
|
|
}
|
2024-11-21 09:20:20 +01:00
|
|
|
}
|
|
|
|
}
|
2024-11-19 12:03:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-20 10:26:32 +02:00
|
|
|
useEffect(() => {
|
|
|
|
if (selectedFiles) {
|
|
|
|
/**
|
2024-11-19 12:03:41 +01:00
|
|
|
* Reads the binary files and converts to an internal file type
|
2024-09-20 10:26:32 +02:00
|
|
|
* and sets to a state (adds images if it's a PDF)
|
|
|
|
*/
|
|
|
|
const parsePages = async () => {
|
|
|
|
const files = await settleAllFullfilfedPromises(
|
|
|
|
selectedFiles,
|
|
|
|
getSigitFile
|
|
|
|
)
|
|
|
|
|
|
|
|
setDrawnFiles(files)
|
|
|
|
}
|
|
|
|
|
|
|
|
setIsParsing(true)
|
|
|
|
|
|
|
|
parsePages().finally(() => {
|
|
|
|
setIsParsing(false)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}, [selectedFiles])
|
2024-07-11 16:16:36 +02:00
|
|
|
|
2024-08-19 18:05:14 +02:00
|
|
|
/**
|
|
|
|
* Changes the drawing tool
|
|
|
|
* @param drawTool to draw with
|
|
|
|
*/
|
|
|
|
const handleToolSelect = (drawTool: DrawTool) => {
|
|
|
|
// If clicked on the same tool, unselect
|
|
|
|
if (drawTool.identifier === selectedTool?.identifier) {
|
|
|
|
setSelectedTool(undefined)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
setSelectedTool(drawTool)
|
|
|
|
}
|
|
|
|
|
2024-07-11 16:16:36 +02:00
|
|
|
useEffect(() => {
|
|
|
|
users.forEach((user) => {
|
|
|
|
if (!(user.pubkey in metadata)) {
|
2024-09-13 18:13:34 +02:00
|
|
|
const metadataController = MetadataController.getInstance()
|
2024-07-11 16:16:36 +02:00
|
|
|
|
|
|
|
const handleMetadataEvent = (event: Event) => {
|
|
|
|
const metadataContent =
|
|
|
|
metadataController.extractProfileMetadataContent(event)
|
|
|
|
if (metadataContent)
|
|
|
|
setMetadata((prev) => ({
|
|
|
|
...prev,
|
|
|
|
[user.pubkey]: metadataContent
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
metadataController.on(user.pubkey, (kind: number, event: Event) => {
|
|
|
|
if (kind === kinds.Metadata) {
|
|
|
|
handleMetadataEvent(event)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
metadataController
|
|
|
|
.findMetadata(user.pubkey)
|
|
|
|
.then((metadataEvent) => {
|
|
|
|
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.error(
|
|
|
|
`error occurred in finding metadata for: ${user.pubkey}`,
|
|
|
|
err
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
2024-08-08 12:49:55 +02:00
|
|
|
}, [metadata, users])
|
2024-07-11 16:16:36 +02:00
|
|
|
|
2024-06-13 11:47:28 +05:00
|
|
|
useEffect(() => {
|
2024-08-08 17:30:49 +02:00
|
|
|
if (uploadedFiles) {
|
|
|
|
setSelectedFiles([...uploadedFiles])
|
2024-06-13 11:47:28 +05:00
|
|
|
}
|
2024-08-08 17:30:49 +02:00
|
|
|
}, [uploadedFiles])
|
2024-06-13 11:47:28 +05:00
|
|
|
|
2024-05-15 13:41:55 +05:00
|
|
|
useEffect(() => {
|
|
|
|
if (usersPubkey) {
|
|
|
|
setUsers((prev) => {
|
|
|
|
const existingUserIndex = prev.findIndex(
|
|
|
|
(user) => user.pubkey === usersPubkey
|
|
|
|
)
|
|
|
|
|
|
|
|
// make logged in user the first signer by default
|
|
|
|
if (existingUserIndex === -1)
|
|
|
|
return [{ pubkey: usersPubkey, role: UserRole.signer }, ...prev]
|
|
|
|
|
|
|
|
return prev
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}, [usersPubkey])
|
|
|
|
|
2024-11-21 09:20:20 +01:00
|
|
|
const handleAddUser = useCallback(async () => {
|
2024-05-14 14:27:05 +05:00
|
|
|
setError(undefined)
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-05-14 14:27:05 +05:00
|
|
|
const addUser = (pubkey: string) => {
|
|
|
|
setUsers((prev) => {
|
2024-05-20 13:28:46 +05:00
|
|
|
const signers = prev.filter((user) => user.role === UserRole.signer)
|
|
|
|
const viewers = prev.filter((user) => user.role === UserRole.viewer)
|
|
|
|
|
2024-05-14 14:27:05 +05:00
|
|
|
const existingUserIndex = prev.findIndex(
|
|
|
|
(user) => user.pubkey === pubkey
|
|
|
|
)
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-05-14 14:27:05 +05:00
|
|
|
// add new
|
2024-05-20 13:28:46 +05:00
|
|
|
if (existingUserIndex === -1) {
|
|
|
|
if (userRole === UserRole.signer) {
|
|
|
|
return [...signers, { pubkey, role: userRole }, ...viewers]
|
|
|
|
} else {
|
|
|
|
return [...signers, ...viewers, { pubkey, role: userRole }]
|
|
|
|
}
|
|
|
|
}
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-05-14 14:27:05 +05:00
|
|
|
const existingUser = prev[existingUserIndex]
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-05-14 14:27:05 +05:00
|
|
|
// return existing
|
|
|
|
if (existingUser.role === userRole) return prev
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-05-14 14:27:05 +05:00
|
|
|
// change user role
|
|
|
|
const updatedUsers = [...prev]
|
|
|
|
const updatedUser = { ...updatedUsers[existingUserIndex] }
|
|
|
|
updatedUser.role = userRole
|
|
|
|
updatedUsers[existingUserIndex] = updatedUser
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-05-20 13:28:46 +05:00
|
|
|
// signers should be placed at the start of the array
|
|
|
|
return [
|
|
|
|
...updatedUsers.filter((user) => user.role === UserRole.signer),
|
|
|
|
...updatedUsers.filter((user) => user.role === UserRole.viewer)
|
|
|
|
]
|
2024-05-14 14:27:05 +05:00
|
|
|
})
|
|
|
|
}
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-06-03 12:13:37 +05:00
|
|
|
const input = userInput.toLowerCase()
|
|
|
|
|
2024-11-21 09:20:20 +01:00
|
|
|
setUserSearchInput('')
|
|
|
|
|
2024-06-03 12:13:37 +05:00
|
|
|
if (input.startsWith('npub')) {
|
2024-10-10 13:56:08 +02:00
|
|
|
return handleAddNpubUser(input)
|
2024-04-18 16:12:11 +05:00
|
|
|
}
|
|
|
|
|
2024-06-03 12:13:37 +05:00
|
|
|
if (input.includes('@')) {
|
2024-10-10 13:56:08 +02:00
|
|
|
return await handleAddNip05User(input)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the user enters the domain (w/o @) assume it's the "root" and append _@
|
|
|
|
// https://github.com/nostr-protocol/nips/blob/master/05.md#showing-just-the-domain-as-an-identifier
|
|
|
|
if (input.includes('.')) {
|
|
|
|
return await handleAddNip05User(`_@${input}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
setError('Invalid input! Make sure to provide correct npub or nip05.')
|
|
|
|
|
|
|
|
async function handleAddNip05User(input: string) {
|
2024-05-14 14:27:05 +05:00
|
|
|
setIsLoading(true)
|
|
|
|
setLoadingSpinnerDesc('Querying for nip05')
|
2024-06-03 12:13:37 +05:00
|
|
|
const nip05Profile = await queryNip05(input)
|
2024-05-14 14:27:05 +05:00
|
|
|
.catch((err) => {
|
2024-06-03 12:13:37 +05:00
|
|
|
console.error(`error occurred in querying nip05: ${input}`, err)
|
2024-05-14 14:27:05 +05:00
|
|
|
return null
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
setIsLoading(false)
|
|
|
|
setLoadingSpinnerDesc('')
|
|
|
|
})
|
|
|
|
|
2024-05-20 00:08:41 +05:00
|
|
|
if (nip05Profile && nip05Profile.pubkey) {
|
2024-05-14 14:27:05 +05:00
|
|
|
const pubkey = nip05Profile.pubkey
|
|
|
|
addUser(pubkey)
|
|
|
|
setUserInput('')
|
|
|
|
} else {
|
|
|
|
setError('Provided nip05 is not valid. Please enter correct nip05.')
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-10-10 13:56:08 +02:00
|
|
|
function handleAddNpubUser(input: string) {
|
|
|
|
const pubkey = npubToHex(input)
|
|
|
|
if (pubkey) {
|
|
|
|
addUser(pubkey)
|
|
|
|
setUserInput('')
|
|
|
|
} else {
|
|
|
|
setError('Provided npub is not valid. Please enter correct npub.')
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2024-11-21 09:20:20 +01:00
|
|
|
}, [
|
|
|
|
userInput,
|
|
|
|
userRole,
|
|
|
|
setError,
|
|
|
|
setUsers,
|
|
|
|
setUserSearchInput,
|
|
|
|
setIsLoading,
|
|
|
|
setLoadingSpinnerDesc,
|
|
|
|
setUserInput
|
|
|
|
])
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (userInput?.length > 0) handleAddUser()
|
|
|
|
}, [handleAddUser, userInput])
|
2024-05-14 14:27:05 +05:00
|
|
|
|
2024-05-20 00:08:41 +05:00
|
|
|
const handleUserRoleChange = (role: UserRole, pubkey: string) => {
|
|
|
|
setUsers((prevUsers) =>
|
|
|
|
prevUsers.map((user) => {
|
|
|
|
if (user.pubkey === pubkey) {
|
|
|
|
return {
|
|
|
|
...user,
|
|
|
|
role
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return user
|
|
|
|
})
|
|
|
|
)
|
2024-05-14 14:27:05 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
const handleRemoveUser = (pubkey: string) => {
|
|
|
|
setUsers((prev) => prev.filter((user) => user.pubkey !== pubkey))
|
2024-09-20 10:26:32 +02:00
|
|
|
|
|
|
|
// Set counterpart to ''
|
|
|
|
const drawnFilesCopy = _.cloneDeep(drawnFiles)
|
|
|
|
drawnFilesCopy.forEach((s) => {
|
|
|
|
s.pages?.forEach((p) => {
|
|
|
|
p.drawnFields.forEach((d) => {
|
|
|
|
if (d.counterpart === hexToNpub(pubkey)) {
|
|
|
|
d.counterpart = ''
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
setDrawnFiles(drawnFilesCopy)
|
2024-05-14 14:27:05 +05:00
|
|
|
}
|
|
|
|
|
2024-05-20 12:19:53 +05:00
|
|
|
/**
|
|
|
|
* changes the position of signer in the signers list
|
|
|
|
*
|
|
|
|
* @param dragIndex represents the current position of user
|
|
|
|
* @param hoverIndex represents the target position of user
|
|
|
|
*/
|
|
|
|
const moveSigner = (dragIndex: number, hoverIndex: number) => {
|
2024-05-20 00:08:41 +05:00
|
|
|
setUsers((prevUsers) => {
|
|
|
|
const updatedUsers = [...prevUsers]
|
|
|
|
const [draggedUser] = updatedUsers.splice(dragIndex, 1)
|
|
|
|
updatedUsers.splice(hoverIndex, 0, draggedUser)
|
|
|
|
return updatedUsers
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-08-19 08:30:22 +02:00
|
|
|
const handleSelectFiles = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
if (event.target.files) {
|
2024-09-17 17:56:53 +02:00
|
|
|
// Get the uploaded files
|
|
|
|
const files = Array.from(event.target.files)
|
|
|
|
|
|
|
|
// Remove duplicates based on the file.name
|
|
|
|
setSelectedFiles((p) =>
|
|
|
|
[...p, ...files].filter(
|
|
|
|
(file, i, array) => i === array.findIndex((t) => t.name === file.name)
|
|
|
|
)
|
|
|
|
)
|
2024-08-19 08:30:22 +02:00
|
|
|
}
|
|
|
|
}
|
2024-05-14 14:27:05 +05:00
|
|
|
|
2024-08-19 14:55:26 +02:00
|
|
|
const handleFileClick = (id: string) => {
|
|
|
|
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' })
|
2024-05-14 14:27:05 +05:00
|
|
|
}
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-08-19 08:30:22 +02:00
|
|
|
const handleRemoveFile = (
|
|
|
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
|
|
|
fileToRemove: File
|
|
|
|
) => {
|
|
|
|
event.stopPropagation()
|
|
|
|
|
2024-05-14 14:27:05 +05:00
|
|
|
setSelectedFiles((prevFiles) =>
|
|
|
|
prevFiles.filter((file) => file.name !== fileToRemove.name)
|
2024-04-22 16:24:50 +05:00
|
|
|
)
|
2024-05-14 14:27:05 +05:00
|
|
|
}
|
2024-04-22 16:24:50 +05:00
|
|
|
|
2024-06-10 18:10:43 +05:00
|
|
|
// Validate inputs before proceeding
|
|
|
|
const validateInputs = (): boolean => {
|
2024-06-03 22:59:51 +05:00
|
|
|
if (!title.trim()) {
|
|
|
|
toast.error('Title can not be empty')
|
2024-06-10 18:10:43 +05:00
|
|
|
return false
|
2024-06-03 22:59:51 +05:00
|
|
|
}
|
|
|
|
|
2024-08-30 13:27:10 +02:00
|
|
|
if (!users.some((u) => u.role === UserRole.signer)) {
|
|
|
|
toast.error('No signer is provided. At least add one signer.')
|
2024-06-10 18:10:43 +05:00
|
|
|
return false
|
2024-05-14 14:27:05 +05:00
|
|
|
}
|
2024-04-22 16:24:50 +05:00
|
|
|
|
2024-05-14 14:27:05 +05:00
|
|
|
if (selectedFiles.length === 0) {
|
|
|
|
toast.error('No file is selected. Select at least 1 file')
|
2024-06-10 18:10:43 +05:00
|
|
|
return false
|
2024-05-14 14:27:05 +05:00
|
|
|
}
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-06-10 18:10:43 +05:00
|
|
|
return true
|
|
|
|
}
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-06-10 18:10:43 +05:00
|
|
|
// Handle errors during file arrayBuffer conversion
|
2024-08-08 12:49:55 +02:00
|
|
|
const handleFileError = (file: File) => (err: unknown) => {
|
2024-06-10 18:10:43 +05:00
|
|
|
console.log(
|
|
|
|
`Error while getting arrayBuffer of file ${file.name} :>> `,
|
|
|
|
err
|
|
|
|
)
|
2024-08-08 12:49:55 +02:00
|
|
|
if (err instanceof Error) {
|
|
|
|
toast.error(
|
|
|
|
err.message || `Error while getting arrayBuffer of file ${file.name}`
|
|
|
|
)
|
|
|
|
}
|
2024-06-10 18:10:43 +05:00
|
|
|
return null
|
|
|
|
}
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-06-10 18:10:43 +05:00
|
|
|
// Generate hash for each selected file
|
|
|
|
const generateFileHashes = async (): Promise<{
|
|
|
|
[key: string]: string
|
|
|
|
} | null> => {
|
2024-04-18 16:12:11 +05:00
|
|
|
const fileHashes: { [key: string]: string } = {}
|
|
|
|
|
2024-05-14 14:27:05 +05:00
|
|
|
for (const file of selectedFiles) {
|
2024-06-10 18:10:43 +05:00
|
|
|
const arraybuffer = await file.arrayBuffer().catch(handleFileError(file))
|
|
|
|
if (!arraybuffer) return null
|
2024-05-14 14:27:05 +05:00
|
|
|
|
|
|
|
const hash = await getHash(arraybuffer)
|
2024-04-18 16:12:11 +05:00
|
|
|
if (!hash) {
|
2024-06-10 18:10:43 +05:00
|
|
|
return null
|
2024-04-18 16:12:11 +05:00
|
|
|
}
|
|
|
|
|
2024-05-14 14:27:05 +05:00
|
|
|
fileHashes[file.name] = hash
|
2024-04-18 16:12:11 +05:00
|
|
|
}
|
|
|
|
|
2024-06-10 18:10:43 +05:00
|
|
|
return fileHashes
|
|
|
|
}
|
|
|
|
|
2024-08-14 12:24:15 +03:00
|
|
|
const createMarks = (fileHashes: { [key: string]: string }): Mark[] => {
|
2024-08-22 18:20:54 +02:00
|
|
|
return drawnFiles
|
|
|
|
.flatMap((file) => {
|
|
|
|
const fileHash = fileHashes[file.name]
|
|
|
|
return (
|
|
|
|
file.pages?.flatMap((page, index) => {
|
|
|
|
return page.drawnFields.map((drawnField) => {
|
2024-08-27 17:58:07 +02:00
|
|
|
if (!drawnField.counterpart) {
|
|
|
|
throw new Error('Missing counterpart')
|
|
|
|
}
|
2024-08-22 18:20:54 +02:00
|
|
|
return {
|
|
|
|
type: drawnField.type,
|
|
|
|
location: {
|
|
|
|
page: index,
|
|
|
|
top: drawnField.top,
|
|
|
|
left: drawnField.left,
|
|
|
|
height: drawnField.height,
|
|
|
|
width: drawnField.width
|
|
|
|
},
|
|
|
|
npub: drawnField.counterpart,
|
|
|
|
pdfFileHash: fileHash,
|
|
|
|
fileName: file.name
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}) || []
|
|
|
|
)
|
2024-07-11 16:16:36 +02:00
|
|
|
})
|
2024-07-18 15:38:07 +03:00
|
|
|
.map((mark, index) => {
|
2024-08-14 12:24:15 +03:00
|
|
|
return { ...mark, id: index }
|
|
|
|
})
|
2024-07-11 16:16:36 +02:00
|
|
|
}
|
|
|
|
|
2024-06-10 18:10:43 +05:00
|
|
|
// Handle errors during zip file generation
|
2024-08-08 12:49:55 +02:00
|
|
|
const handleZipError = (err: unknown) => {
|
2024-06-10 18:10:43 +05:00
|
|
|
console.log('Error in zip:>> ', err)
|
|
|
|
setIsLoading(false)
|
2024-08-08 12:49:55 +02:00
|
|
|
if (err instanceof Error) {
|
|
|
|
toast.error(err.message || 'Error occurred in generating zip file')
|
|
|
|
}
|
2024-06-10 18:10:43 +05:00
|
|
|
return null
|
|
|
|
}
|
2024-04-22 16:24:50 +05:00
|
|
|
|
2024-06-10 18:10:43 +05:00
|
|
|
// Generate the zip file
|
|
|
|
const generateZipFile = async (zip: JSZip): Promise<ArrayBuffer | null> => {
|
2024-05-14 14:27:05 +05:00
|
|
|
setLoadingSpinnerDesc('Generating zip file')
|
2024-04-22 16:24:50 +05:00
|
|
|
|
2024-08-05 14:11:15 +03:00
|
|
|
return await zip
|
2024-04-18 16:12:11 +05:00
|
|
|
.generateAsync({
|
|
|
|
type: 'arraybuffer',
|
|
|
|
compression: 'DEFLATE',
|
2024-06-10 18:10:43 +05:00
|
|
|
compressionOptions: { level: 6 }
|
2024-04-18 16:12:11 +05:00
|
|
|
})
|
2024-06-10 18:10:43 +05:00
|
|
|
.catch(handleZipError)
|
|
|
|
}
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-06-10 18:10:43 +05:00
|
|
|
// Encrypt the zip file with the generated encryption key
|
|
|
|
const encryptZipFile = async (
|
|
|
|
arraybuffer: ArrayBuffer,
|
|
|
|
encryptionKey: string
|
|
|
|
): Promise<ArrayBuffer> => {
|
2024-04-18 16:12:11 +05:00
|
|
|
setLoadingSpinnerDesc('Encrypting zip file')
|
2024-06-12 15:02:26 +05:00
|
|
|
return encryptArrayBuffer(arraybuffer, encryptionKey)
|
|
|
|
}
|
|
|
|
|
2024-07-05 13:38:04 +05:00
|
|
|
// create final zip file for offline mode
|
2024-06-12 15:02:26 +05:00
|
|
|
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 firstSigner = users.filter((user) => user.role === UserRole.signer)[0]
|
|
|
|
|
|
|
|
const keysFileContent = await generateKeysFile(
|
|
|
|
[firstSigner.pubkey],
|
2024-04-18 16:12:11 +05:00
|
|
|
encryptionKey
|
2024-06-10 18:10:43 +05:00
|
|
|
)
|
2024-06-12 15:02:26 +05:00
|
|
|
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
|
|
|
|
|
2024-08-20 17:37:32 +02:00
|
|
|
return new File([new Blob([arraybuffer])], `${unixNow()}.sigit.zip`, {
|
2024-08-14 12:24:15 +03:00
|
|
|
type: 'application/zip'
|
|
|
|
})
|
2024-06-10 18:10:43 +05:00
|
|
|
}
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-06-10 18:10:43 +05:00
|
|
|
// Handle errors during file upload
|
2024-08-08 12:49:55 +02:00
|
|
|
const handleUploadError = (err: unknown) => {
|
2024-06-10 18:10:43 +05:00
|
|
|
console.log('Error in upload:>> ', err)
|
|
|
|
setIsLoading(false)
|
2024-08-08 12:49:55 +02:00
|
|
|
if (err instanceof Error) {
|
|
|
|
toast.error(err.message || 'Error occurred in uploading file')
|
|
|
|
}
|
2024-06-10 18:10:43 +05:00
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2024-06-12 19:44:06 +05:00
|
|
|
// Upload the file to the storage
|
2024-07-05 13:38:04 +05:00
|
|
|
const uploadFile = async (
|
|
|
|
arrayBuffer: ArrayBuffer
|
|
|
|
): Promise<string | null> => {
|
|
|
|
const blob = new Blob([arrayBuffer])
|
|
|
|
// Create a File object with the Blob data
|
2024-08-13 11:52:05 +02:00
|
|
|
const file = new File([blob], `compressed-${unixNow()}.sigit`, {
|
2024-07-05 13:38:04 +05:00
|
|
|
type: 'application/sigit'
|
|
|
|
})
|
2024-04-18 16:12:11 +05:00
|
|
|
|
2024-08-05 14:11:15 +03:00
|
|
|
return await uploadToFileStorage(file)
|
2024-06-10 18:10:43 +05:00
|
|
|
.then((url) => {
|
2024-07-05 13:38:04 +05:00
|
|
|
toast.success('files.zip uploaded to file storage')
|
2024-06-10 18:10:43 +05:00
|
|
|
return url
|
|
|
|
})
|
|
|
|
.catch(handleUploadError)
|
|
|
|
}
|
2024-05-14 14:27:05 +05:00
|
|
|
|
2024-06-10 18:10:43 +05:00
|
|
|
// Manage offline scenarios for signing or viewing the file
|
2024-06-12 19:44:06 +05:00
|
|
|
const handleOfflineFlow = async (
|
|
|
|
encryptedArrayBuffer: ArrayBuffer,
|
|
|
|
encryptionKey: string
|
|
|
|
) => {
|
|
|
|
const finalZipFile = await createFinalZipFile(
|
|
|
|
encryptedArrayBuffer,
|
|
|
|
encryptionKey
|
|
|
|
)
|
2024-06-03 14:01:24 +05:00
|
|
|
|
2024-08-08 12:49:55 +02:00
|
|
|
if (!finalZipFile) {
|
|
|
|
setIsLoading(false)
|
|
|
|
return
|
|
|
|
}
|
2024-06-12 19:44:06 +05:00
|
|
|
|
2024-08-13 11:52:05 +02:00
|
|
|
saveAs(finalZipFile, `request-${unixNow()}.sigit.zip`)
|
2024-09-13 18:13:34 +02:00
|
|
|
|
|
|
|
// If user is the next signer, we can navigate directly to sign page
|
|
|
|
if (signers[0].pubkey === usersPubkey) {
|
|
|
|
navigate(appPrivateRoutes.sign, { state: { uploadedZip: finalZipFile } })
|
|
|
|
}
|
2024-08-08 12:49:55 +02:00
|
|
|
setIsLoading(false)
|
2024-04-18 16:12:11 +05:00
|
|
|
}
|
|
|
|
|
2024-07-05 13:38:04 +05:00
|
|
|
const generateFilesZip = async (): Promise<ArrayBuffer | null> => {
|
|
|
|
const zip = new JSZip()
|
|
|
|
selectedFiles.forEach((file) => {
|
|
|
|
zip.file(file.name, file)
|
|
|
|
})
|
|
|
|
|
2024-08-05 14:11:15 +03:00
|
|
|
return await zip
|
2024-07-05 13:38:04 +05:00
|
|
|
.generateAsync({
|
|
|
|
type: 'arraybuffer',
|
|
|
|
compression: 'DEFLATE',
|
|
|
|
compressionOptions: { level: 6 }
|
|
|
|
})
|
|
|
|
.catch(handleZipError)
|
|
|
|
}
|
|
|
|
|
|
|
|
const generateCreateSignature = async (
|
2024-08-27 17:58:07 +02:00
|
|
|
markConfig: Mark[],
|
2024-07-05 13:38:04 +05:00
|
|
|
fileHashes: {
|
|
|
|
[key: string]: string
|
|
|
|
},
|
2024-09-27 14:18:26 +03:00
|
|
|
zipUrl: string
|
2024-07-05 13:38:04 +05:00
|
|
|
) => {
|
|
|
|
const content: CreateSignatureEventContent = {
|
|
|
|
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
|
|
|
|
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
|
|
|
|
fileHashes,
|
2024-07-12 09:15:52 +02:00
|
|
|
markConfig,
|
2024-07-05 13:38:04 +05:00
|
|
|
zipUrl,
|
2024-09-27 14:18:26 +03:00
|
|
|
title
|
2024-07-05 13:38:04 +05:00
|
|
|
}
|
|
|
|
|
2024-12-03 11:25:31 +01:00
|
|
|
setLoadingSpinnerDesc('Preparing document(s) for signing')
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-07-08 14:26:36 +05:00
|
|
|
const createSignature = await signEventForMetaFile(
|
|
|
|
JSON.stringify(content),
|
|
|
|
nostrController,
|
|
|
|
setIsLoading
|
|
|
|
).catch(() => {
|
|
|
|
console.log('An error occurred in signing event for meta file', error)
|
|
|
|
toast.error('An error occurred in signing event for meta file')
|
|
|
|
return null
|
|
|
|
})
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-07-08 14:26:36 +05:00
|
|
|
if (!createSignature) return null
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-07-08 14:26:36 +05:00
|
|
|
return JSON.stringify(createSignature, null, 2)
|
2024-07-05 13:38:04 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Send notifications to signers and viewers
|
2024-12-06 20:00:38 +01:00
|
|
|
const sendNotifications = (notification: SigitNotification) => {
|
2024-07-05 13:38:04 +05:00
|
|
|
// 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)
|
|
|
|
|
2024-12-06 20:00:38 +01:00
|
|
|
return receivers.map((receiver) => sendNotification(receiver, notification))
|
2024-07-05 13:38:04 +05:00
|
|
|
}
|
|
|
|
|
2024-09-27 14:18:26 +03:00
|
|
|
const extractNostrId = (stringifiedEvent: string): string => {
|
|
|
|
const e = JSON.parse(stringifiedEvent) as SignedEvent
|
|
|
|
return e.id
|
|
|
|
}
|
|
|
|
|
2024-06-10 18:10:43 +05:00
|
|
|
const handleCreate = async () => {
|
2024-08-27 17:58:07 +02:00
|
|
|
try {
|
|
|
|
if (!validateInputs()) return
|
2024-06-10 18:10:43 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
setIsLoading(true)
|
|
|
|
setLoadingSpinnerDesc('Generating file hashes')
|
|
|
|
const fileHashes = await generateFileHashes()
|
2024-08-29 12:54:51 +02:00
|
|
|
if (!fileHashes) return
|
2024-06-10 18:10:43 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
setLoadingSpinnerDesc('Generating encryption key')
|
|
|
|
const encryptionKey = await generateEncryptionKey()
|
2024-06-10 18:10:43 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
if (await isOnline()) {
|
|
|
|
setLoadingSpinnerDesc('generating files.zip')
|
|
|
|
const arrayBuffer = await generateFilesZip()
|
2024-08-29 12:54:51 +02:00
|
|
|
if (!arrayBuffer) return
|
2024-06-10 18:10:43 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
setLoadingSpinnerDesc('Encrypting files.zip')
|
|
|
|
const encryptedArrayBuffer = await encryptZipFile(
|
|
|
|
arrayBuffer,
|
|
|
|
encryptionKey
|
|
|
|
)
|
2024-06-10 18:10:43 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
const markConfig = createMarks(fileHashes)
|
2024-06-10 18:10:43 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
setLoadingSpinnerDesc('Uploading files.zip to file storage')
|
|
|
|
const fileUrl = await uploadFile(encryptedArrayBuffer)
|
2024-08-29 12:54:51 +02:00
|
|
|
if (!fileUrl) return
|
2024-06-12 15:02:26 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
setLoadingSpinnerDesc('Generating create signature')
|
|
|
|
const createSignature = await generateCreateSignature(
|
|
|
|
markConfig,
|
|
|
|
fileHashes,
|
2024-09-27 14:18:26 +03:00
|
|
|
fileUrl
|
2024-08-27 17:58:07 +02:00
|
|
|
)
|
2024-08-29 12:54:51 +02:00
|
|
|
if (!createSignature) return
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
setLoadingSpinnerDesc('Generating keys for decryption')
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
// 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!)
|
|
|
|
}
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
const keys = await generateKeys(pubkeys, encryptionKey)
|
2024-08-29 12:54:51 +02:00
|
|
|
if (!keys) return
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-10-07 17:20:00 +02:00
|
|
|
setLoadingSpinnerDesc('Generating an open timestamp.')
|
|
|
|
|
2024-09-27 14:18:26 +03:00
|
|
|
const timestamp = await generateTimestamp(
|
|
|
|
extractNostrId(createSignature)
|
|
|
|
)
|
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
const meta: Meta = {
|
|
|
|
createSignature,
|
|
|
|
keys,
|
|
|
|
modifiedAt: unixNow(),
|
|
|
|
docSignatures: {}
|
|
|
|
}
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-09-27 14:18:26 +03:00
|
|
|
if (timestamp) {
|
|
|
|
meta.timestamps = [timestamp]
|
|
|
|
}
|
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
setLoadingSpinnerDesc('Updating user app data')
|
2024-12-06 20:00:38 +01:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
const event = await updateUsersAppData(meta)
|
2024-08-29 12:54:51 +02:00
|
|
|
if (!event) return
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-12-13 12:39:00 +01:00
|
|
|
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
setLoadingSpinnerDesc('Sending notifications to counterparties')
|
2024-12-06 20:00:38 +01:00
|
|
|
const promises = sendNotifications({
|
|
|
|
metaUrl,
|
|
|
|
keys: meta.keys
|
|
|
|
})
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
await Promise.all(promises)
|
|
|
|
.then(() => {
|
|
|
|
toast.success('Notifications sent successfully')
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
toast.error('Failed to publish notifications')
|
|
|
|
})
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-12-24 10:58:29 +01:00
|
|
|
const isFirstSigner = signers[0].pubkey === usersPubkey
|
|
|
|
|
|
|
|
if (isFirstSigner) {
|
|
|
|
navigate(appPrivateRoutes.sign, { state: { meta } })
|
|
|
|
} else {
|
|
|
|
const createSignatureJson = JSON.parse(createSignature)
|
|
|
|
navigate(`${appPublicRoutes.verify}/${createSignatureJson.id}`)
|
|
|
|
}
|
2024-08-27 17:58:07 +02:00
|
|
|
} else {
|
|
|
|
const zip = new JSZip()
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
selectedFiles.forEach((file) => {
|
|
|
|
zip.file(`files/${file.name}`, file)
|
|
|
|
})
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
const markConfig = createMarks(fileHashes)
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
setLoadingSpinnerDesc('Generating create signature')
|
|
|
|
const createSignature = await generateCreateSignature(
|
|
|
|
markConfig,
|
|
|
|
fileHashes,
|
|
|
|
''
|
|
|
|
)
|
2024-08-29 12:54:51 +02:00
|
|
|
if (!createSignature) return
|
2024-08-27 17:58:07 +02:00
|
|
|
|
|
|
|
const meta: Meta = {
|
|
|
|
createSignature,
|
|
|
|
modifiedAt: unixNow(),
|
|
|
|
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
|
|
|
|
}
|
2024-07-05 13:38:04 +05:00
|
|
|
|
2024-08-27 17:58:07 +02:00
|
|
|
const arrayBuffer = await generateZipFile(zip)
|
2024-08-29 12:54:51 +02:00
|
|
|
if (!arrayBuffer) return
|
2024-08-27 17:58:07 +02:00
|
|
|
|
|
|
|
setLoadingSpinnerDesc('Encrypting zip file')
|
|
|
|
const encryptedArrayBuffer = await encryptZipFile(
|
|
|
|
arrayBuffer,
|
|
|
|
encryptionKey
|
|
|
|
)
|
|
|
|
|
|
|
|
await handleOfflineFlow(encryptedArrayBuffer, encryptionKey)
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
if (error instanceof Error) {
|
|
|
|
toast.error(error.message)
|
|
|
|
}
|
|
|
|
console.error(error)
|
|
|
|
} finally {
|
|
|
|
setIsLoading(false)
|
2024-05-28 15:10:06 +05:00
|
|
|
}
|
2024-04-18 16:12:11 +05:00
|
|
|
}
|
|
|
|
|
2024-11-21 09:20:20 +01:00
|
|
|
/**
|
|
|
|
* Handles the user search textfield change
|
|
|
|
* If it's not valid npub or nip05, search will be automatically triggered
|
|
|
|
*/
|
|
|
|
const handleSearchAutocompleteTextfieldChange = async (
|
|
|
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
|
|
) => {
|
|
|
|
const value = e.target.value
|
|
|
|
|
|
|
|
const disarmAddOnEnter = () => {
|
|
|
|
setPastedUserNpubOrNip05(undefined)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Seems like it's npub format
|
|
|
|
if (value.startsWith('npub')) {
|
|
|
|
// We will try to convert npub to hex and if it's successfull that means
|
|
|
|
// npub is valid
|
|
|
|
const validHexPubkey = npubToHex(value)
|
|
|
|
|
|
|
|
if (validHexPubkey) {
|
|
|
|
// Arm the manual user npub add after enter is hit, we don't want to trigger search
|
|
|
|
setPastedUserNpubOrNip05(value)
|
|
|
|
} else {
|
|
|
|
disarmAddOnEnter()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Disarm the add user on enter hit, and trigger search after 1 second
|
|
|
|
disarmAddOnEnter()
|
|
|
|
}
|
|
|
|
|
|
|
|
setUserSearchInput(value)
|
|
|
|
}
|
|
|
|
|
2024-11-19 12:03:41 +01:00
|
|
|
const parseContent = (event: Event) => {
|
|
|
|
try {
|
|
|
|
return JSON.parse(event.content)
|
|
|
|
} catch (e) {
|
2024-11-21 09:20:20 +01:00
|
|
|
return undefined
|
2024-11-19 12:03:41 +01:00
|
|
|
console.error(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-18 16:12:11 +05:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
2024-08-06 12:46:42 +03:00
|
|
|
<Container className={styles.container}>
|
2024-08-15 18:57:40 +02:00
|
|
|
<StickySideColumns
|
|
|
|
left={
|
2024-08-16 11:08:03 +02:00
|
|
|
<div className={styles.flexWrap}>
|
2024-08-19 08:30:22 +02:00
|
|
|
<div className={styles.inputWrapper}>
|
|
|
|
<TextField
|
2024-09-05 13:24:34 +02:00
|
|
|
fullWidth
|
2024-08-19 08:30:22 +02:00
|
|
|
placeholder="Title"
|
|
|
|
size="small"
|
|
|
|
type="text"
|
|
|
|
value={title}
|
|
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<ol className={`${styles.paperGroup} ${styles.orderedFilesList}`}>
|
2024-08-16 11:08:03 +02:00
|
|
|
{selectedFiles.length > 0 &&
|
|
|
|
selectedFiles.map((file, index) => (
|
2024-08-29 12:54:51 +02:00
|
|
|
<li
|
2024-08-19 08:30:22 +02:00
|
|
|
key={index}
|
2024-08-19 14:55:26 +02:00
|
|
|
className={`${fileListStyles.fileItem} ${isActive(file) && fileListStyles.active}`}
|
|
|
|
onClick={() => {
|
|
|
|
handleFileClick('file-' + file.name)
|
|
|
|
setCurrentFile(file)
|
|
|
|
}}
|
2024-08-19 08:30:22 +02:00
|
|
|
>
|
2024-08-29 12:54:51 +02:00
|
|
|
<span className={styles.fileName}>{file.name}</span>
|
|
|
|
<Button
|
|
|
|
aria-label={`delete ${file.name}`}
|
|
|
|
variant="text"
|
|
|
|
onClick={(event) => handleRemoveFile(event, file)}
|
|
|
|
>
|
|
|
|
<FontAwesomeIcon icon={faTrash} />
|
|
|
|
</Button>
|
|
|
|
</li>
|
2024-08-16 11:08:03 +02:00
|
|
|
))}
|
2024-08-19 08:30:22 +02:00
|
|
|
</ol>
|
|
|
|
<Button variant="contained" onClick={handleUploadButtonClick}>
|
|
|
|
<FontAwesomeIcon icon={faUpload} />
|
2024-08-20 09:23:42 +02:00
|
|
|
<span className={styles.uploadFileText}>Upload new files</span>
|
2024-08-19 08:30:22 +02:00
|
|
|
</Button>
|
2024-08-29 12:54:51 +02:00
|
|
|
<input
|
|
|
|
ref={fileInputRef}
|
|
|
|
hidden={true}
|
|
|
|
multiple={true}
|
|
|
|
type="file"
|
|
|
|
aria-label="file-upload"
|
|
|
|
onChange={handleSelectFiles}
|
|
|
|
/>
|
2024-08-16 11:08:03 +02:00
|
|
|
</div>
|
2024-08-15 18:57:40 +02:00
|
|
|
}
|
|
|
|
right={
|
2024-08-16 11:08:03 +02:00
|
|
|
<div className={styles.flexWrap}>
|
2024-09-03 13:38:55 +02:00
|
|
|
<div className={`${styles.paperGroup} ${styles.users}`}>
|
|
|
|
<DisplayUser
|
|
|
|
users={users}
|
|
|
|
handleUserRoleChange={handleUserRoleChange}
|
|
|
|
handleRemoveUser={handleRemoveUser}
|
|
|
|
moveSigner={moveSigner}
|
|
|
|
/>
|
|
|
|
</div>
|
2024-11-19 12:03:41 +01:00
|
|
|
|
|
|
|
<div className={styles.addCounterpart}>
|
|
|
|
<div className={styles.inputWrapper}>
|
|
|
|
<Autocomplete
|
|
|
|
sx={{ width: 300 }}
|
|
|
|
options={foundUsers}
|
|
|
|
onChange={handleSearchUserChange}
|
2024-11-21 09:20:20 +01:00
|
|
|
inputValue={userSearchInput}
|
|
|
|
disableClearable
|
|
|
|
openOnFocus
|
2024-11-19 12:03:41 +01:00
|
|
|
autoHighlight
|
|
|
|
freeSolo
|
|
|
|
filterOptions={(x) => x}
|
|
|
|
getOptionLabel={(option) => {
|
2024-11-21 09:20:20 +01:00
|
|
|
let label: string = (option as FoundUser).npub
|
|
|
|
|
|
|
|
const contentJson = parseContent(option as FoundUser)
|
2024-11-19 12:03:41 +01:00
|
|
|
|
2024-11-21 09:20:20 +01:00
|
|
|
if (contentJson?.name) {
|
|
|
|
label = contentJson.name
|
|
|
|
} else {
|
|
|
|
label = option as string
|
|
|
|
}
|
2024-11-19 12:03:41 +01:00
|
|
|
|
|
|
|
return label
|
|
|
|
}}
|
|
|
|
renderOption={(props, option) => {
|
|
|
|
const { ...optionProps } = props
|
|
|
|
|
|
|
|
const contentJson = parseContent(option)
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Box
|
|
|
|
component="li"
|
|
|
|
sx={{ '& > img': { mr: 2, flexShrink: 0 } }}
|
|
|
|
{...optionProps}
|
|
|
|
key={option.pubkey}
|
|
|
|
>
|
2024-11-21 09:20:20 +01:00
|
|
|
<AvatarIconButton
|
|
|
|
src={contentJson.picture}
|
|
|
|
hexKey={option.pubkey}
|
|
|
|
color="inherit"
|
|
|
|
sx={{
|
|
|
|
padding: '0 10px 0 0'
|
|
|
|
}}
|
2024-11-19 12:03:41 +01:00
|
|
|
/>
|
2024-11-21 09:20:20 +01:00
|
|
|
|
|
|
|
<div>
|
|
|
|
{contentJson.name}{' '}
|
|
|
|
{usersPubkey === option.pubkey ? (
|
|
|
|
<span
|
|
|
|
style={{
|
|
|
|
color: '#4c82a3',
|
|
|
|
fontWeight: 'bold'
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Me
|
|
|
|
</span>
|
|
|
|
) : (
|
|
|
|
''
|
|
|
|
)}{' '}
|
|
|
|
({truncate(option.npub, { length: 16 })})
|
|
|
|
</div>
|
2024-11-19 12:03:41 +01:00
|
|
|
</Box>
|
|
|
|
)
|
|
|
|
}}
|
|
|
|
renderInput={(params) => (
|
|
|
|
<TextField
|
|
|
|
{...params}
|
|
|
|
key={params.id}
|
2024-11-21 09:20:20 +01:00
|
|
|
inputRef={searchFieldRef}
|
|
|
|
label="Add/Search counterpart"
|
|
|
|
onKeyDown={handleInputKeyDown}
|
|
|
|
onChange={handleSearchAutocompleteTextfieldChange}
|
2024-11-19 12:03:41 +01:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
</div>
|
2024-11-21 09:20:20 +01:00
|
|
|
{!pastedUserNpubOrNip05 ? (
|
|
|
|
<Button
|
|
|
|
disabled={!userSearchInput || searchUsersLoading}
|
|
|
|
onClick={() => handleSearchUsers()}
|
|
|
|
variant="contained"
|
|
|
|
aria-label="Add"
|
|
|
|
className={styles.counterpartToggleButton}
|
|
|
|
>
|
|
|
|
{searchUsersLoading ? (
|
|
|
|
<CircularProgress size={14} />
|
|
|
|
) : (
|
|
|
|
<FontAwesomeIcon icon={faSearch} />
|
|
|
|
)}
|
|
|
|
</Button>
|
|
|
|
) : (
|
|
|
|
<Button
|
2024-12-10 15:29:24 +01:00
|
|
|
onClick={() => {
|
|
|
|
setUserInput(userSearchInput)
|
|
|
|
}}
|
2024-11-21 09:20:20 +01:00
|
|
|
variant="contained"
|
|
|
|
aria-label="Add"
|
|
|
|
className={styles.counterpartToggleButton}
|
|
|
|
>
|
|
|
|
<FontAwesomeIcon icon={faPlus} />
|
|
|
|
</Button>
|
|
|
|
)}
|
2024-08-19 08:30:22 +02:00
|
|
|
</div>
|
2024-08-20 11:46:24 +02:00
|
|
|
|
2024-08-19 18:05:14 +02:00
|
|
|
<div className={`${styles.paperGroup} ${styles.toolbox}`}>
|
2024-09-19 13:15:54 +02:00
|
|
|
{DEFAULT_TOOLBOX.filter((drawTool) => !drawTool.isHidden).map(
|
|
|
|
(drawTool: DrawTool, index: number) => {
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
key={index}
|
|
|
|
{...(!drawTool.isComingSoon && {
|
|
|
|
onClick: () => handleToolSelect(drawTool)
|
|
|
|
})}
|
|
|
|
className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${drawTool.isComingSoon ? styles.comingSoon : ''}
|
2024-08-19 18:05:14 +02:00
|
|
|
`}
|
2024-09-19 13:15:54 +02:00
|
|
|
>
|
|
|
|
<FontAwesomeIcon
|
|
|
|
fontSize={'15px'}
|
|
|
|
icon={drawTool.icon}
|
|
|
|
/>
|
|
|
|
{drawTool.label}
|
|
|
|
{!drawTool.isComingSoon ? (
|
|
|
|
<FontAwesomeIcon
|
|
|
|
fontSize={'15px'}
|
|
|
|
icon={faEllipsis}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<span className={styles.comingSoonPlaceholder}>
|
|
|
|
Coming soon
|
|
|
|
</span>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)}
|
2024-08-19 18:05:14 +02:00
|
|
|
</div>
|
|
|
|
|
2024-09-12 13:30:59 +02:00
|
|
|
<Button onClick={handleCreate} variant="contained">
|
|
|
|
Publish
|
|
|
|
</Button>
|
|
|
|
|
2024-08-20 11:46:24 +02:00
|
|
|
{!!error && (
|
|
|
|
<FormHelperText error={!!error}>{error}</FormHelperText>
|
|
|
|
)}
|
2024-08-16 11:08:03 +02:00
|
|
|
</div>
|
2024-08-15 18:57:40 +02:00
|
|
|
}
|
2024-09-04 14:05:36 +02:00
|
|
|
leftIcon={faFileCirclePlus}
|
|
|
|
centerIcon={faFile}
|
|
|
|
rightIcon={faToolbox}
|
2024-08-15 18:57:40 +02:00
|
|
|
>
|
2024-09-20 10:26:32 +02:00
|
|
|
{parsingPdf ? (
|
|
|
|
<LoadingSpinner variant="small" />
|
|
|
|
) : (
|
|
|
|
<DrawPDFFields
|
|
|
|
users={users}
|
|
|
|
metadata={metadata}
|
|
|
|
selectedTool={selectedTool}
|
|
|
|
sigitFiles={drawnFiles}
|
|
|
|
setSigitFiles={setDrawnFiles}
|
|
|
|
/>
|
|
|
|
)}
|
2024-08-15 18:57:40 +02:00
|
|
|
</StickySideColumns>
|
2024-08-06 12:46:42 +03:00
|
|
|
</Container>
|
2024-04-18 16:12:11 +05:00
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
2024-05-14 14:27:05 +05:00
|
|
|
|
|
|
|
type DisplayUsersProps = {
|
|
|
|
users: User[]
|
2024-05-20 00:08:41 +05:00
|
|
|
handleUserRoleChange: (role: UserRole, pubkey: string) => void
|
2024-05-14 14:27:05 +05:00
|
|
|
handleRemoveUser: (pubkey: string) => void
|
2024-05-20 00:08:41 +05:00
|
|
|
moveSigner: (dragIndex: number, hoverIndex: number) => void
|
2024-05-14 14:27:05 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
const DisplayUser = ({
|
|
|
|
users,
|
|
|
|
handleUserRoleChange,
|
2024-05-20 00:08:41 +05:00
|
|
|
handleRemoveUser,
|
|
|
|
moveSigner
|
2024-05-14 14:27:05 +05:00
|
|
|
}: DisplayUsersProps) => {
|
|
|
|
return (
|
2024-08-19 08:30:22 +02:00
|
|
|
<>
|
2024-09-05 13:24:34 +02:00
|
|
|
<DndProvider backend={MultiBackend} options={HTML5toTouch}>
|
2024-08-19 08:30:22 +02:00
|
|
|
{users
|
|
|
|
.filter((user) => user.role === UserRole.signer)
|
|
|
|
.map((user, index) => (
|
2024-09-03 13:38:55 +02:00
|
|
|
<SignerCounterpart
|
|
|
|
key={`signer-${user.pubkey}`}
|
2024-08-19 08:30:22 +02:00
|
|
|
user={user}
|
|
|
|
index={index}
|
|
|
|
moveSigner={moveSigner}
|
|
|
|
handleUserRoleChange={handleUserRoleChange}
|
|
|
|
handleRemoveUser={handleRemoveUser}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</DndProvider>
|
|
|
|
{users
|
|
|
|
.filter((user) => user.role === UserRole.viewer)
|
2024-09-03 13:38:55 +02:00
|
|
|
.map((user) => {
|
2024-08-19 08:30:22 +02:00
|
|
|
return (
|
2024-09-03 13:38:55 +02:00
|
|
|
<div className={styles.user} key={`viewer-${user.pubkey}`}>
|
|
|
|
<Counterpart
|
|
|
|
user={user}
|
|
|
|
handleUserRoleChange={handleUserRoleChange}
|
|
|
|
handleRemoveUser={handleRemoveUser}
|
|
|
|
/>
|
2024-08-19 08:30:22 +02:00
|
|
|
</div>
|
|
|
|
)
|
|
|
|
})}
|
|
|
|
</>
|
2024-05-14 14:27:05 +05:00
|
|
|
)
|
|
|
|
}
|
2024-05-20 00:08:41 +05:00
|
|
|
|
|
|
|
interface DragItem {
|
|
|
|
index: number
|
|
|
|
id: string
|
|
|
|
type: string
|
|
|
|
}
|
|
|
|
|
2024-09-03 13:38:55 +02:00
|
|
|
type CounterpartProps = {
|
2024-05-20 00:08:41 +05:00
|
|
|
user: User
|
|
|
|
handleUserRoleChange: (role: UserRole, pubkey: string) => void
|
|
|
|
handleRemoveUser: (pubkey: string) => void
|
|
|
|
}
|
|
|
|
|
2024-09-03 13:38:55 +02:00
|
|
|
type SignerCounterpartProps = CounterpartProps & {
|
|
|
|
index: number
|
|
|
|
moveSigner: (dragIndex: number, hoverIndex: number) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
const SignerCounterpart = ({
|
2024-05-20 00:08:41 +05:00
|
|
|
user,
|
|
|
|
index,
|
|
|
|
moveSigner,
|
|
|
|
handleUserRoleChange,
|
|
|
|
handleRemoveUser
|
2024-09-03 13:38:55 +02:00
|
|
|
}: SignerCounterpartProps) => {
|
2024-05-20 00:08:41 +05:00
|
|
|
const ref = useRef<HTMLTableRowElement>(null)
|
|
|
|
|
|
|
|
const [{ handlerId }, drop] = useDrop<
|
|
|
|
DragItem,
|
|
|
|
void,
|
|
|
|
{ handlerId: Identifier | null }
|
|
|
|
>({
|
|
|
|
accept: 'row',
|
|
|
|
collect(monitor) {
|
|
|
|
return {
|
|
|
|
handlerId: monitor.getHandlerId()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
hover(item: DragItem, monitor) {
|
|
|
|
if (!ref.current) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const dragIndex = item.index
|
|
|
|
const hoverIndex = index
|
|
|
|
|
|
|
|
// Don't replace items with themselves
|
|
|
|
if (dragIndex === hoverIndex) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine rectangle on screen
|
|
|
|
const hoverBoundingRect = ref.current?.getBoundingClientRect()
|
|
|
|
|
|
|
|
// Get vertical middle
|
|
|
|
const hoverMiddleY =
|
|
|
|
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
|
|
|
|
|
|
|
|
// Determine mouse position
|
|
|
|
const clientOffset = monitor.getClientOffset()
|
|
|
|
|
|
|
|
// Get pixels to the top
|
|
|
|
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top
|
|
|
|
|
|
|
|
// Only perform the move when the mouse has crossed half of the items height
|
|
|
|
// When dragging downwards, only move when the cursor is below 50%
|
|
|
|
// When dragging upwards, only move when the cursor is above 50%
|
|
|
|
|
|
|
|
// Dragging downwards
|
|
|
|
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Dragging upwards
|
|
|
|
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Time to actually perform the action
|
|
|
|
moveSigner(dragIndex, hoverIndex)
|
|
|
|
|
|
|
|
// Note: we're mutating the monitor item here!
|
|
|
|
// Generally it's better to avoid mutations,
|
|
|
|
// but it's good here for the sake of performance
|
|
|
|
// to avoid expensive index searches.
|
|
|
|
item.index = hoverIndex
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const [{ isDragging }, drag] = useDrag({
|
|
|
|
type: 'row',
|
|
|
|
item: () => {
|
|
|
|
return { id: user.pubkey, index }
|
|
|
|
},
|
2024-08-08 17:30:49 +02:00
|
|
|
collect: (monitor) => ({
|
2024-05-20 00:08:41 +05:00
|
|
|
isDragging: monitor.isDragging()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2024-09-05 09:28:04 +02:00
|
|
|
const opacity = isDragging ? 0.2 : 1
|
2024-05-20 00:08:41 +05:00
|
|
|
drag(drop(ref))
|
|
|
|
|
|
|
|
return (
|
2024-08-19 08:30:22 +02:00
|
|
|
<div
|
|
|
|
className={styles.user}
|
|
|
|
style={{ cursor: 'move', opacity }}
|
2024-05-20 00:08:41 +05:00
|
|
|
data-handler-id={handlerId}
|
|
|
|
ref={ref}
|
|
|
|
>
|
2024-08-20 11:46:24 +02:00
|
|
|
<FontAwesomeIcon width={'14px'} fontSize={'14px'} icon={faGripLines} />
|
2024-09-03 13:38:55 +02:00
|
|
|
<Counterpart
|
|
|
|
user={user}
|
|
|
|
handleRemoveUser={handleRemoveUser}
|
|
|
|
handleUserRoleChange={handleUserRoleChange}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const Counterpart = ({
|
|
|
|
user,
|
|
|
|
handleUserRoleChange,
|
|
|
|
handleRemoveUser
|
|
|
|
}: CounterpartProps) => {
|
|
|
|
return (
|
|
|
|
<>
|
2024-08-19 08:30:22 +02:00
|
|
|
<div className={styles.avatar}>
|
2024-09-13 18:20:10 +02:00
|
|
|
<UserAvatar pubkey={user.pubkey} isNameVisible={true} />
|
2024-08-19 08:30:22 +02:00
|
|
|
</div>
|
2024-09-03 13:38:55 +02:00
|
|
|
<Tooltip title="Toggle User Role" arrow disableInteractive>
|
|
|
|
<Button
|
|
|
|
onClick={() =>
|
|
|
|
handleUserRoleChange(
|
|
|
|
user.role === UserRole.signer ? UserRole.viewer : UserRole.signer,
|
|
|
|
user.pubkey
|
|
|
|
)
|
|
|
|
}
|
2024-09-05 13:24:34 +02:00
|
|
|
className={styles.counterpartRowToggleButton}
|
|
|
|
data-variant="primary"
|
2024-09-03 13:38:55 +02:00
|
|
|
>
|
2024-08-20 11:46:24 +02:00
|
|
|
<FontAwesomeIcon
|
2024-09-03 13:38:55 +02:00
|
|
|
icon={user.role === UserRole.signer ? faPen : faEye}
|
2024-08-20 11:46:24 +02:00
|
|
|
/>
|
2024-09-03 13:38:55 +02:00
|
|
|
</Button>
|
|
|
|
</Tooltip>
|
|
|
|
<Tooltip title="Remove User" arrow disableInteractive>
|
2024-08-20 11:46:24 +02:00
|
|
|
<Button
|
|
|
|
onClick={() => handleRemoveUser(user.pubkey)}
|
2024-09-05 13:24:34 +02:00
|
|
|
className={styles.counterpartRowToggleButton}
|
|
|
|
data-variant="secondary"
|
2024-08-20 11:46:24 +02:00
|
|
|
>
|
2024-09-05 13:24:34 +02:00
|
|
|
<FontAwesomeIcon icon={faTrash} />
|
2024-08-20 11:46:24 +02:00
|
|
|
</Button>
|
2024-08-19 08:30:22 +02:00
|
|
|
</Tooltip>
|
2024-09-03 13:38:55 +02:00
|
|
|
</>
|
2024-05-20 00:08:41 +05:00
|
|
|
)
|
|
|
|
}
|