2024-06-28 14:24:14 +05:00
|
|
|
import { Add, CalendarMonth, Description, Upload } from '@mui/icons-material'
|
2024-06-08 00:37:03 +05:00
|
|
|
import { Box, Button, Tooltip, Typography } from '@mui/material'
|
2024-06-28 14:24:14 +05:00
|
|
|
import JSZip from 'jszip'
|
2024-07-05 13:38:04 +05:00
|
|
|
import { Event, kinds, verifyEvent } from 'nostr-tools'
|
2024-06-28 14:24:14 +05:00
|
|
|
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
|
2024-05-14 14:27:05 +05:00
|
|
|
import { useNavigate } from 'react-router-dom'
|
2024-06-28 14:24:14 +05:00
|
|
|
import { toast } from 'react-toastify'
|
2024-07-05 13:38:04 +05:00
|
|
|
import { UserComponent } from '../../components/username'
|
|
|
|
import { MetadataController } from '../../controllers'
|
|
|
|
import { useAppSelector } from '../../hooks'
|
2024-06-13 11:47:28 +05:00
|
|
|
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
2024-07-05 13:38:04 +05:00
|
|
|
import { CreateSignatureEventContent, Meta, ProfileMetadata } from '../../types'
|
2024-06-28 14:24:14 +05:00
|
|
|
import {
|
|
|
|
formatTimestamp,
|
|
|
|
hexToNpub,
|
|
|
|
npubToHex,
|
|
|
|
parseJson,
|
|
|
|
shorten
|
|
|
|
} from '../../utils'
|
2024-07-05 13:38:04 +05:00
|
|
|
import styles from './style.module.scss'
|
2024-04-08 17:45:51 +05:00
|
|
|
|
|
|
|
export const HomePage = () => {
|
2024-05-14 14:27:05 +05:00
|
|
|
const navigate = useNavigate()
|
2024-06-13 11:47:28 +05:00
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
2024-07-05 13:38:04 +05:00
|
|
|
const [sigits, setSigits] = useState<Meta[]>([])
|
2024-06-28 14:24:14 +05:00
|
|
|
const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>(
|
|
|
|
{}
|
|
|
|
)
|
2024-07-05 13:38:04 +05:00
|
|
|
const usersAppData = useAppSelector((state) => state.userAppData)
|
2024-06-28 14:24:14 +05:00
|
|
|
|
|
|
|
useEffect(() => {
|
2024-07-05 13:38:04 +05:00
|
|
|
if (usersAppData) {
|
|
|
|
setSigits(Object.values(usersAppData.sigits))
|
|
|
|
}
|
|
|
|
}, [usersAppData])
|
2024-06-13 11:47:28 +05:00
|
|
|
|
|
|
|
const handleUploadClick = () => {
|
|
|
|
if (fileInputRef.current) {
|
|
|
|
fileInputRef.current.click()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleFileChange = async (
|
|
|
|
event: React.ChangeEvent<HTMLInputElement>
|
|
|
|
) => {
|
|
|
|
const file = event.target.files?.[0]
|
|
|
|
if (file) {
|
|
|
|
// Check if the file extension is .sigit.zip
|
|
|
|
const fileName = file.name
|
|
|
|
const fileExtension = fileName.slice(-10) // ".sigit.zip" has 10 characters
|
|
|
|
if (fileExtension === '.sigit.zip') {
|
|
|
|
const zip = await JSZip.loadAsync(file).catch((err) => {
|
|
|
|
console.log('err in loading zip file :>> ', err)
|
|
|
|
toast.error(err.message || 'An error occurred in loading zip file.')
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!zip) return
|
|
|
|
|
|
|
|
// navigate to sign page if zip contains keys.json
|
|
|
|
if ('keys.json' in zip.files) {
|
|
|
|
return navigate(appPrivateRoutes.sign, {
|
|
|
|
state: { uploadedZip: file }
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// navigate to verify page if zip contains meta.json
|
|
|
|
if ('meta.json' in zip.files) {
|
|
|
|
return navigate(appPublicRoutes.verify, {
|
|
|
|
state: { uploadedZip: file }
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
toast.error('Invalid zip file')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// navigate to create page
|
|
|
|
navigate(appPrivateRoutes.create, { state: { uploadedFile: file } })
|
|
|
|
}
|
|
|
|
}
|
2024-04-08 17:45:51 +05:00
|
|
|
|
|
|
|
return (
|
2024-06-28 14:24:14 +05:00
|
|
|
<>
|
|
|
|
<Box className={styles.container}>
|
|
|
|
<Box className={styles.header}>
|
|
|
|
<Typography variant="h3" className={styles.title}>
|
|
|
|
Sigits
|
|
|
|
</Typography>
|
|
|
|
{/* This is for desktop view */}
|
|
|
|
<Box
|
|
|
|
className={styles.actionButtons}
|
|
|
|
sx={{
|
|
|
|
display: {
|
|
|
|
xs: 'none',
|
|
|
|
md: 'flex'
|
|
|
|
}
|
|
|
|
}}
|
2024-06-07 16:13:32 +05:00
|
|
|
>
|
2024-06-28 14:24:14 +05:00
|
|
|
<input
|
|
|
|
type="file"
|
|
|
|
ref={fileInputRef}
|
|
|
|
style={{ display: 'none' }}
|
|
|
|
onChange={handleFileChange}
|
|
|
|
/>
|
2024-06-08 00:37:03 +05:00
|
|
|
<Button
|
|
|
|
variant="outlined"
|
2024-06-28 14:24:14 +05:00
|
|
|
startIcon={<Upload />}
|
|
|
|
onClick={handleUploadClick}
|
2024-06-08 00:37:03 +05:00
|
|
|
>
|
2024-06-28 14:24:14 +05:00
|
|
|
Upload
|
2024-06-08 00:37:03 +05:00
|
|
|
</Button>
|
|
|
|
<Button
|
|
|
|
variant="contained"
|
2024-06-28 14:24:14 +05:00
|
|
|
startIcon={<Add />}
|
2024-06-08 00:37:03 +05:00
|
|
|
onClick={() => navigate(appPrivateRoutes.create)}
|
|
|
|
>
|
2024-06-28 14:24:14 +05:00
|
|
|
Create
|
2024-06-08 00:37:03 +05:00
|
|
|
</Button>
|
2024-06-28 14:24:14 +05:00
|
|
|
</Box>
|
|
|
|
{/* This is for mobile view */}
|
|
|
|
<Box
|
|
|
|
className={styles.actionButtons}
|
|
|
|
sx={{
|
|
|
|
display: {
|
|
|
|
xs: 'flex',
|
|
|
|
md: 'none'
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Tooltip title="Upload" arrow>
|
|
|
|
<Button
|
|
|
|
variant="outlined"
|
|
|
|
onClick={() => navigate(appPrivateRoutes.sign)}
|
|
|
|
>
|
|
|
|
<Upload />
|
|
|
|
</Button>
|
|
|
|
</Tooltip>
|
|
|
|
<Tooltip title="Create" arrow>
|
|
|
|
<Button
|
|
|
|
variant="contained"
|
|
|
|
onClick={() => navigate(appPrivateRoutes.create)}
|
|
|
|
>
|
|
|
|
<Add />
|
|
|
|
</Button>
|
|
|
|
</Tooltip>
|
|
|
|
</Box>
|
|
|
|
</Box>
|
|
|
|
<Box className={styles.submissions}>
|
2024-07-05 13:38:04 +05:00
|
|
|
{sigits.map((sigit, index) => (
|
2024-06-28 14:24:14 +05:00
|
|
|
<DisplaySigit
|
2024-07-05 13:38:04 +05:00
|
|
|
key={`sigit-${index}`}
|
|
|
|
meta={sigit}
|
2024-06-28 14:24:14 +05:00
|
|
|
profiles={profiles}
|
|
|
|
setProfiles={setProfiles}
|
|
|
|
/>
|
|
|
|
))}
|
2024-06-08 00:37:03 +05:00
|
|
|
</Box>
|
|
|
|
</Box>
|
2024-06-28 14:24:14 +05:00
|
|
|
</>
|
2024-06-07 16:13:32 +05:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-06-28 14:24:14 +05:00
|
|
|
type SigitProps = {
|
2024-07-05 13:38:04 +05:00
|
|
|
meta: Meta
|
2024-06-28 14:24:14 +05:00
|
|
|
profiles: { [key: string]: ProfileMetadata }
|
|
|
|
setProfiles: Dispatch<SetStateAction<{ [key: string]: ProfileMetadata }>>
|
|
|
|
}
|
|
|
|
|
2024-07-05 13:38:04 +05:00
|
|
|
enum SignedStatus {
|
|
|
|
Partial = 'Partially Signed',
|
|
|
|
Complete = 'Completely Signed'
|
|
|
|
}
|
|
|
|
|
|
|
|
const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => {
|
2024-06-28 14:24:14 +05:00
|
|
|
const navigate = useNavigate()
|
|
|
|
|
2024-07-05 13:38:04 +05:00
|
|
|
const [title, setTitle] = useState<string>()
|
2024-06-28 14:24:14 +05:00
|
|
|
const [createdAt, setCreatedAt] = useState('')
|
|
|
|
const [submittedBy, setSubmittedBy] = useState<string>()
|
|
|
|
const [signers, setSigners] = useState<`npub1${string}`[]>([])
|
2024-07-05 13:38:04 +05:00
|
|
|
const [signedStatus, setSignedStatus] = useState<SignedStatus>(
|
|
|
|
SignedStatus.Partial
|
|
|
|
)
|
2024-06-28 14:24:14 +05:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const extractInfo = async () => {
|
|
|
|
const createSignatureEvent = await parseJson<Event>(
|
2024-07-05 13:38:04 +05:00
|
|
|
meta.createSignature
|
2024-06-28 14:24:14 +05:00
|
|
|
).catch((err) => {
|
|
|
|
console.log('err in parsing the createSignature event:>> ', err)
|
|
|
|
toast.error(
|
|
|
|
err.message || 'error occurred in parsing the create signature event'
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!createSignatureEvent) return
|
|
|
|
|
|
|
|
// created_at in nostr events are stored in seconds
|
|
|
|
// convert it to ms before formatting
|
|
|
|
setCreatedAt(formatTimestamp(createSignatureEvent.created_at * 1000))
|
|
|
|
|
|
|
|
const createSignatureContent =
|
|
|
|
await parseJson<CreateSignatureEventContent>(
|
|
|
|
createSignatureEvent.content
|
|
|
|
).catch((err) => {
|
|
|
|
console.log(
|
|
|
|
`err in parsing the createSignature event's content :>> `,
|
|
|
|
err
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!createSignatureContent) return
|
|
|
|
|
2024-07-05 13:38:04 +05:00
|
|
|
setTitle(createSignatureContent.title)
|
2024-06-28 14:24:14 +05:00
|
|
|
setSubmittedBy(createSignatureEvent.pubkey)
|
|
|
|
setSigners(createSignatureContent.signers)
|
2024-07-05 13:38:04 +05:00
|
|
|
|
|
|
|
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
|
|
|
const isCompletelySigned = createSignatureContent.signers.every(
|
|
|
|
(signer) => signedBy.includes(signer)
|
|
|
|
)
|
|
|
|
if (isCompletelySigned) {
|
|
|
|
setSignedStatus(SignedStatus.Complete)
|
|
|
|
}
|
2024-06-28 14:24:14 +05:00
|
|
|
}
|
|
|
|
extractInfo()
|
2024-07-05 13:38:04 +05:00
|
|
|
}, [meta])
|
2024-06-28 14:24:14 +05:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const hexKeys: string[] = []
|
|
|
|
|
|
|
|
if (submittedBy) {
|
|
|
|
hexKeys.push(npubToHex(submittedBy)!)
|
|
|
|
}
|
|
|
|
hexKeys.push(...signers.map((signer) => npubToHex(signer)!))
|
|
|
|
|
|
|
|
const metadataController = new MetadataController()
|
|
|
|
hexKeys.forEach((key) => {
|
|
|
|
if (!(key in profiles)) {
|
|
|
|
const handleMetadataEvent = (event: Event) => {
|
|
|
|
const metadataContent =
|
|
|
|
metadataController.extractProfileMetadataContent(event)
|
|
|
|
|
|
|
|
if (metadataContent)
|
|
|
|
setProfiles((prev) => ({
|
|
|
|
...prev,
|
|
|
|
[key]: metadataContent
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
metadataController.on(key, (kind: number, event: Event) => {
|
|
|
|
if (kind === kinds.Metadata) {
|
|
|
|
handleMetadataEvent(event)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
metadataController
|
|
|
|
.findMetadata(key)
|
|
|
|
.then((metadataEvent) => {
|
|
|
|
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.error(`error occurred in finding metadata for: ${key}`, err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}, [submittedBy, signers])
|
|
|
|
|
|
|
|
const handleNavigation = () => {
|
2024-07-05 13:38:04 +05:00
|
|
|
if (signedStatus === SignedStatus.Complete) {
|
|
|
|
navigate(appPublicRoutes.verify, { state: { meta } })
|
|
|
|
} else {
|
|
|
|
navigate(appPrivateRoutes.sign, { state: { meta } })
|
|
|
|
}
|
2024-06-28 14:24:14 +05:00
|
|
|
}
|
|
|
|
|
2024-06-07 16:13:32 +05:00
|
|
|
return (
|
2024-06-08 00:37:03 +05:00
|
|
|
<Box
|
|
|
|
className={styles.item}
|
|
|
|
sx={{
|
|
|
|
flexDirection: {
|
|
|
|
xs: 'column',
|
|
|
|
md: 'row'
|
|
|
|
}
|
|
|
|
}}
|
2024-06-28 14:24:14 +05:00
|
|
|
onClick={handleNavigation}
|
2024-06-08 00:37:03 +05:00
|
|
|
>
|
|
|
|
<Box
|
|
|
|
className={styles.titleBox}
|
|
|
|
sx={{
|
|
|
|
borderBottomLeftRadius: {
|
|
|
|
xs: 'initial',
|
|
|
|
md: 'inherit'
|
|
|
|
},
|
|
|
|
borderTopRightRadius: {
|
|
|
|
xs: 'inherit',
|
|
|
|
md: 'initial'
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
2024-06-28 14:24:14 +05:00
|
|
|
<Typography variant="body1" className={styles.title}>
|
2024-06-08 00:37:03 +05:00
|
|
|
<Description />
|
2024-07-05 13:38:04 +05:00
|
|
|
{title}
|
2024-06-08 00:37:03 +05:00
|
|
|
</Typography>
|
2024-06-28 14:24:14 +05:00
|
|
|
{submittedBy &&
|
|
|
|
(function () {
|
|
|
|
const profile = profiles[submittedBy]
|
|
|
|
return (
|
|
|
|
<UserComponent
|
|
|
|
pubkey={submittedBy}
|
|
|
|
name={
|
|
|
|
profile?.display_name ||
|
|
|
|
profile?.name ||
|
|
|
|
shorten(hexToNpub(submittedBy))
|
|
|
|
}
|
|
|
|
image={profile?.picture}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
})()}
|
|
|
|
<Typography variant="body2" className={styles.date}>
|
2024-06-08 00:37:03 +05:00
|
|
|
<CalendarMonth />
|
2024-06-28 14:24:14 +05:00
|
|
|
{createdAt}
|
2024-06-08 00:37:03 +05:00
|
|
|
</Typography>
|
|
|
|
</Box>
|
|
|
|
<Box className={styles.signers}>
|
2024-06-28 14:24:14 +05:00
|
|
|
{signers.map((signer) => {
|
|
|
|
const pubkey = npubToHex(signer)!
|
|
|
|
const profile = profiles[pubkey]
|
|
|
|
|
|
|
|
return (
|
|
|
|
<DisplaySigner
|
|
|
|
key={signer}
|
2024-07-05 13:38:04 +05:00
|
|
|
meta={meta}
|
2024-06-28 14:24:14 +05:00
|
|
|
profile={profile}
|
|
|
|
pubkey={pubkey}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
})}
|
2024-06-07 16:13:32 +05:00
|
|
|
</Box>
|
2024-05-14 14:27:05 +05:00
|
|
|
</Box>
|
2024-04-08 17:45:51 +05:00
|
|
|
)
|
|
|
|
}
|
2024-06-28 14:24:14 +05:00
|
|
|
|
|
|
|
enum SignStatus {
|
|
|
|
Signed = 'Signed',
|
|
|
|
Pending = 'Pending',
|
|
|
|
Invalid = 'Invalid Sign'
|
|
|
|
}
|
|
|
|
|
|
|
|
type DisplaySignerProps = {
|
|
|
|
meta: Meta
|
|
|
|
profile: ProfileMetadata
|
|
|
|
pubkey: string
|
|
|
|
}
|
|
|
|
|
|
|
|
const DisplaySigner = ({ meta, profile, pubkey }: DisplaySignerProps) => {
|
|
|
|
const [signStatus, setSignedStatus] = useState<SignStatus>()
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const updateSignStatus = async () => {
|
|
|
|
const npub = hexToNpub(pubkey)
|
|
|
|
if (npub in meta.docSignatures) {
|
|
|
|
parseJson<Event>(meta.docSignatures[npub])
|
|
|
|
.then((event) => {
|
|
|
|
const isValidSignature = verifyEvent(event)
|
|
|
|
if (isValidSignature) {
|
|
|
|
setSignedStatus(SignStatus.Signed)
|
|
|
|
} else {
|
|
|
|
setSignedStatus(SignStatus.Invalid)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.log(`err in parsing the docSignatures for ${npub}:>> `, err)
|
|
|
|
setSignedStatus(SignStatus.Invalid)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
setSignedStatus(SignStatus.Pending)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateSignStatus()
|
|
|
|
}, [meta, pubkey])
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Box className={styles.signerItem}>
|
|
|
|
<Typography variant="button" className={styles.status}>
|
|
|
|
{signStatus}
|
|
|
|
</Typography>
|
|
|
|
<UserComponent
|
|
|
|
pubkey={pubkey}
|
|
|
|
name={
|
|
|
|
profile?.display_name || profile?.name || shorten(hexToNpub(pubkey))
|
|
|
|
}
|
|
|
|
image={profile?.picture}
|
|
|
|
/>
|
|
|
|
</Box>
|
|
|
|
)
|
|
|
|
}
|