sigit.io/src/pages/home/index.tsx

402 lines
11 KiB
TypeScript
Raw Normal View History

2024-06-28 14:24:14 +05:00
import { Add, CalendarMonth, Description, Upload } from '@mui/icons-material'
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'
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'
export const HomePage = () => {
2024-05-14 14:27:05 +05:00
const navigate = useNavigate()
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])
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 } })
}
}
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}
/>
<Button
variant="outlined"
2024-06-28 14:24:14 +05:00
startIcon={<Upload />}
onClick={handleUploadClick}
>
2024-06-28 14:24:14 +05:00
Upload
</Button>
<Button
variant="contained"
2024-06-28 14:24:14 +05:00
startIcon={<Add />}
onClick={() => navigate(appPrivateRoutes.create)}
>
2024-06-28 14:24:14 +05:00
Create
</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}
/>
))}
</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 (
<Box
className={styles.item}
sx={{
flexDirection: {
xs: 'column',
md: 'row'
}
}}
2024-06-28 14:24:14 +05:00
onClick={handleNavigation}
>
<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}>
<Description />
2024-07-05 13:38:04 +05:00
{title}
</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}>
<CalendarMonth />
2024-06-28 14:24:14 +05:00
{createdAt}
</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-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>
)
}