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

413 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'
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'
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
2024-06-07 16:13:32 +05:00
import styles from './style.module.scss'
2024-06-28 14:24:14 +05:00
import { MetadataController, NostrController } from '../../controllers'
import {
formatTimestamp,
getUsersAppData,
hexToNpub,
npubToHex,
parseJson,
shorten
} from '../../utils'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import {
CreateSignatureEventContent,
Meta,
ProfileMetadata,
Sigit
} from '../../types'
import { Event, kinds, verifyEvent } from 'nostr-tools'
import { UserComponent } from '../../components/username'
export const HomePage = () => {
2024-05-14 14:27:05 +05:00
const navigate = useNavigate()
const fileInputRef = useRef<HTMLInputElement>(null)
2024-06-28 14:24:14 +05:00
const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc] = useState(`Finding user's app data`)
const [authUrl, setAuthUrl] = useState<string>()
const [sigits, setSigits] = useState<Sigit[]>([])
const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>(
{}
)
useEffect(() => {
const nostrController = NostrController.getInstance()
// Set up event listener for authentication event
nostrController.on('nsecbunker-auth', (url) => {
setAuthUrl(url)
})
getUsersAppData()
.then((res) => {
if (res) {
setSigits(Object.values(res))
}
})
.finally(() => {
setIsLoading(false)
})
}, [])
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-06-28 14:24:14 +05:00
if (authUrl) {
return (
<iframe
title="Nsecbunker auth"
src={authUrl}
width="100%"
height="500px"
/>
)
}
return (
2024-06-28 14:24:14 +05:00
<>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
<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}>
{sigits.map((sigit) => (
<DisplaySigit
key={sigit.meta.uuid}
sigit={sigit}
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 = {
sigit: Sigit
profiles: { [key: string]: ProfileMetadata }
setProfiles: Dispatch<SetStateAction<{ [key: string]: ProfileMetadata }>>
}
const DisplaySigit = ({ sigit, profiles, setProfiles }: SigitProps) => {
const navigate = useNavigate()
const [createdAt, setCreatedAt] = useState('')
const [submittedBy, setSubmittedBy] = useState<string>()
const [signers, setSigners] = useState<`npub1${string}`[]>([])
useEffect(() => {
const extractInfo = async () => {
const createSignatureEvent = await parseJson<Event>(
sigit.meta.createSignature
).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
setSubmittedBy(createSignatureEvent.pubkey)
setSigners(createSignatureContent.signers)
}
extractInfo()
}, [sigit])
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 = () => {
navigate(appPrivateRoutes.sign, { state: { sigit } })
}
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-06-28 14:24:14 +05:00
{sigit.meta.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}
meta={sigit.meta}
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>
)
}