Releasing new design #161
246
src/components/FilesUsers.tsx/index.tsx
Normal file
246
src/components/FilesUsers.tsx/index.tsx
Normal file
@ -0,0 +1,246 @@
|
||||
import { CheckCircle, Cancel } from '@mui/icons-material'
|
||||
import { Divider, Tooltip } from '@mui/material'
|
||||
import { verifyEvent } from 'nostr-tools'
|
||||
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
|
||||
import { Meta, SignedEventContent } from '../../types'
|
||||
import {
|
||||
extractFileExtensions,
|
||||
formatTimestamp,
|
||||
fromUnixTimestamp,
|
||||
hexToNpub,
|
||||
npubToHex,
|
||||
shorten
|
||||
} from '../../utils'
|
||||
import { UserAvatar } from '../UserAvatar'
|
||||
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
||||
import { UserAvatarGroup } from '../UserAvatarGroup'
|
||||
|
||||
import styles from './style.module.scss'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
faCalendar,
|
||||
faCalendarCheck,
|
||||
faCalendarPlus,
|
||||
faEye,
|
||||
faFile,
|
||||
faFileCircleExclamation
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { State } from '../../store/rootReducer'
|
||||
|
||||
interface FileUsersProps {
|
||||
meta: Meta
|
||||
}
|
||||
|
||||
export const FileUsers = ({ meta }: FileUsersProps) => {
|
||||
const { usersPubkey } = useSelector((state: State) => state.auth)
|
||||
const {
|
||||
submittedBy,
|
||||
signers,
|
||||
viewers,
|
||||
fileHashes,
|
||||
sig,
|
||||
docSignatures,
|
||||
parsedSignatureEvents,
|
||||
createdAt,
|
||||
signedStatus,
|
||||
completedAt
|
||||
} = useSigitMeta(meta)
|
||||
const profiles = useSigitProfiles([
|
||||
...(submittedBy ? [submittedBy] : []),
|
||||
...signers,
|
||||
...viewers
|
||||
])
|
||||
const userCanSign =
|
||||
typeof usersPubkey !== 'undefined' &&
|
||||
signers.includes(hexToNpub(usersPubkey))
|
||||
|
||||
const ext = extractFileExtensions(Object.keys(fileHashes))
|
||||
|
||||
const getPrevSignersSig = (npub: string) => {
|
||||
// if user is first signer then use creator's signature
|
||||
if (signers[0] === npub) {
|
||||
return sig
|
||||
}
|
||||
|
||||
// find the index of signer
|
||||
const currentSignerIndex = signers.findIndex((signer) => signer === npub)
|
||||
// return null if could not found user in signer's list
|
||||
if (currentSignerIndex === -1) return null
|
||||
// find prev signer
|
||||
const prevSigner = signers[currentSignerIndex - 1]
|
||||
|
||||
// get the signature of prev signer
|
||||
try {
|
||||
const prevSignersEvent = parsedSignatureEvents[prevSigner]
|
||||
return prevSignersEvent.sig
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const displayUser = (pubkey: string, verifySignature = false) => {
|
||||
const profile = profiles[pubkey]
|
||||
|
||||
let isValidSignature = false
|
||||
|
||||
if (verifySignature) {
|
||||
const npub = hexToNpub(pubkey)
|
||||
const signedEventString = docSignatures[npub]
|
||||
if (signedEventString) {
|
||||
try {
|
||||
const signedEvent = JSON.parse(signedEventString)
|
||||
const isVerifiedEvent = verifyEvent(signedEvent)
|
||||
|
||||
if (isVerifiedEvent) {
|
||||
// get the actual signature of prev signer
|
||||
const prevSignersSig = getPrevSignersSig(npub)
|
||||
|
||||
// get the signature of prev signer from the content of current signers signedEvent
|
||||
|
||||
try {
|
||||
const obj: SignedEventContent = JSON.parse(signedEvent.content)
|
||||
if (
|
||||
obj.prevSig &&
|
||||
prevSignersSig &&
|
||||
obj.prevSig === prevSignersSig
|
||||
) {
|
||||
isValidSignature = true
|
||||
}
|
||||
} catch (error) {
|
||||
isValidSignature = false
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`An error occurred in parsing and verifying the signature event for ${pubkey}`,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserAvatar
|
||||
pubkey={pubkey}
|
||||
name={
|
||||
profile?.display_name || profile?.name || shorten(hexToNpub(pubkey))
|
||||
}
|
||||
image={profile?.picture}
|
||||
/>
|
||||
|
||||
{verifySignature && (
|
||||
<>
|
||||
{isValidSignature && (
|
||||
<Tooltip title="Valid signature">
|
||||
<CheckCircle sx={{ color: 'green' }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!isValidSignature && (
|
||||
<Tooltip title="Invalid signature">
|
||||
<Cancel sx={{ color: 'red' }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return submittedBy ? (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.section}>
|
||||
<p>Signers</p>
|
||||
{displayUser(submittedBy)}
|
||||
{submittedBy && signers.length ? (
|
||||
<Divider orientation="vertical" flexItem />
|
||||
) : null}
|
||||
<UserAvatarGroup max={7}>
|
||||
{signers.length > 0 &&
|
||||
signers.map((signer) => (
|
||||
<span key={signer}>{displayUser(npubToHex(signer)!, true)}</span>
|
||||
))}
|
||||
{viewers.length > 0 &&
|
||||
viewers.map((viewer) => (
|
||||
<span key={viewer}>{displayUser(npubToHex(viewer)!)}</span>
|
||||
))}
|
||||
</UserAvatarGroup>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<p>Details</p>
|
||||
|
||||
<Tooltip
|
||||
title={'Publication date'}
|
||||
placement="top"
|
||||
arrow
|
||||
disableInteractive
|
||||
>
|
||||
<span className={styles.detailsItem}>
|
||||
<FontAwesomeIcon icon={faCalendarPlus} />{' '}
|
||||
{createdAt ? formatTimestamp(createdAt) : <>—</>}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
title={'Completion date'}
|
||||
placement="top"
|
||||
arrow
|
||||
disableInteractive
|
||||
>
|
||||
<span className={styles.detailsItem}>
|
||||
<FontAwesomeIcon icon={faCalendarCheck} />{' '}
|
||||
{completedAt ? formatTimestamp(completedAt) : <>—</>}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{/* User signed date */}
|
||||
{userCanSign ? (
|
||||
<Tooltip
|
||||
title={'Your signature date'}
|
||||
placement="top"
|
||||
arrow
|
||||
disableInteractive
|
||||
>
|
||||
<span className={styles.detailsItem}>
|
||||
<FontAwesomeIcon icon={faCalendar} />{' '}
|
||||
{hexToNpub(usersPubkey) in parsedSignatureEvents ? (
|
||||
parsedSignatureEvents[hexToNpub(usersPubkey)].created_at ? (
|
||||
formatTimestamp(
|
||||
fromUnixTimestamp(
|
||||
parsedSignatureEvents[hexToNpub(usersPubkey)].created_at
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<>—</>
|
||||
)
|
||||
) : (
|
||||
<>—</>
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<span className={styles.detailsItem}>
|
||||
<FontAwesomeIcon icon={faEye} /> {signedStatus}
|
||||
</span>
|
||||
{ext.length > 0 ? (
|
||||
<span className={styles.detailsItem}>
|
||||
{ext.length > 1 ? (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faFile} /> Multiple File Types
|
||||
</>
|
||||
) : (
|
||||
getExtensionIconLabel(ext[0])
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faFileCircleExclamation} /> —
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
41
src/components/FilesUsers.tsx/style.module.scss
Normal file
41
src/components/FilesUsers.tsx/style.module.scss
Normal file
@ -0,0 +1,41 @@
|
||||
@import '../../styles/colors.scss';
|
||||
|
||||
.container {
|
||||
border-radius: 4px;
|
||||
background: $overlay-background-color;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: 25px;
|
||||
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
.detailsItem {
|
||||
transition: ease 0.2s;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
|
||||
> :first-child {
|
||||
padding: 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $primary-main;
|
||||
color: white;
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import { Mark } from '../types/mark'
|
||||
import {
|
||||
fromUnixTimestamp,
|
||||
hexToNpub,
|
||||
parseCreateSignatureEvent,
|
||||
parseNostrEvent,
|
||||
parseCreateSignatureEventContent,
|
||||
SigitMetaParseError,
|
||||
SigitStatus,
|
||||
@ -21,7 +21,7 @@ import { NostrController } from '../controllers'
|
||||
* Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,
|
||||
* and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions)
|
||||
*/
|
||||
interface FlatMeta
|
||||
export interface FlatMeta
|
||||
extends Meta,
|
||||
CreateSignatureEventContent,
|
||||
Partial<Omit<Event, 'pubkey' | 'created_at'>> {
|
||||
@ -37,6 +37,12 @@ interface FlatMeta
|
||||
// Decryption
|
||||
encryptionKey: string | null
|
||||
|
||||
// Parsed Document Signatures
|
||||
parsedSignatureEvents: { [signer: `npub1${string}`]: Event }
|
||||
|
||||
// Calculated completion time
|
||||
completedAt?: number
|
||||
|
||||
// Calculated status fields
|
||||
signedStatus: SigitStatus
|
||||
signersStatus: {
|
||||
@ -67,6 +73,12 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
const [title, setTitle] = useState<string>('')
|
||||
const [zipUrl, setZipUrl] = useState<string>('')
|
||||
|
||||
const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{
|
||||
[signer: `npub1${string}`]: Event
|
||||
}>({})
|
||||
|
||||
const [completedAt, setCompletedAt] = useState<number>()
|
||||
|
||||
const [signedStatus, setSignedStatus] = useState<SigitStatus>(
|
||||
SigitStatus.Partial
|
||||
)
|
||||
@ -80,9 +92,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
if (!meta) return
|
||||
;(async function () {
|
||||
try {
|
||||
const createSignatureEvent = await parseCreateSignatureEvent(
|
||||
meta.createSignature
|
||||
)
|
||||
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
||||
|
||||
const { kind, tags, created_at, pubkey, id, sig, content } =
|
||||
createSignatureEvent
|
||||
@ -131,13 +141,22 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse each signature event and set signer status
|
||||
// Temp. map to hold events
|
||||
const parsedSignatureEventsMap = new Map<`npub1${string}`, Event>()
|
||||
for (const npub in meta.docSignatures) {
|
||||
try {
|
||||
const event = await parseCreateSignatureEvent(
|
||||
// Parse each signature event
|
||||
const event = await parseNostrEvent(
|
||||
meta.docSignatures[npub as `npub1${string}`]
|
||||
)
|
||||
|
||||
const isValidSignature = verifyEvent(event)
|
||||
|
||||
// Save events to a map, to save all at once outside loop
|
||||
// We need the object to find completedAt
|
||||
// Avoided using parsedSignatureEvents due to useEffect deps
|
||||
parsedSignatureEventsMap.set(npub as `npub1${string}`, event)
|
||||
|
||||
setSignersStatus((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
@ -155,6 +174,11 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setParsedSignatureEvents(
|
||||
Object.fromEntries(parsedSignatureEventsMap.entries())
|
||||
)
|
||||
|
||||
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
||||
const isCompletelySigned = signers.every((signer) =>
|
||||
signedBy.includes(signer)
|
||||
@ -162,6 +186,20 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
setSignedStatus(
|
||||
isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial
|
||||
)
|
||||
|
||||
// Check if all signers signed and are valid
|
||||
if (isCompletelySigned) {
|
||||
setCompletedAt(
|
||||
fromUnixTimestamp(
|
||||
signedBy.reduce((p, c) => {
|
||||
return Math.max(
|
||||
p,
|
||||
parsedSignatureEventsMap.get(c)?.created_at || 0
|
||||
)
|
||||
}, 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SigitMetaParseError) {
|
||||
toast.error(error.message)
|
||||
@ -189,6 +227,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
markConfig,
|
||||
title,
|
||||
zipUrl,
|
||||
parsedSignatureEvents,
|
||||
completedAt,
|
||||
signedStatus,
|
||||
signersStatus,
|
||||
encryptionKey
|
||||
|
@ -1,36 +1,20 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
List,
|
||||
ListItem,
|
||||
ListSubheader,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material'
|
||||
import { Box, Button, Tooltip, Typography, useTheme } from '@mui/material'
|
||||
import JSZip from 'jszip'
|
||||
import { MuiFileInput } from 'mui-file-input'
|
||||
import { Event, verifyEvent } from 'nostr-tools'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { UserAvatar } from '../../components/UserAvatar'
|
||||
import { NostrController } from '../../controllers'
|
||||
import {
|
||||
CreateSignatureEventContent,
|
||||
Meta,
|
||||
SignedEventContent
|
||||
} from '../../types'
|
||||
import { CreateSignatureEventContent, Meta } from '../../types'
|
||||
import {
|
||||
decryptArrayBuffer,
|
||||
extractMarksFromSignedMeta,
|
||||
getHash,
|
||||
hexToNpub,
|
||||
unixNow,
|
||||
npubToHex,
|
||||
parseJson,
|
||||
readContentOfZipEntry,
|
||||
shorten,
|
||||
signEventForMetaFile
|
||||
} from '../../utils'
|
||||
import styles from './style.module.scss'
|
||||
@ -50,7 +34,8 @@ import { getLastSignersSig } from '../../utils/sign.ts'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { Container } from '../../components/Container'
|
||||
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
||||
import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx'
|
||||
import { Files } from '../../layouts/Files.tsx'
|
||||
import { FileUsers } from '../../components/FilesUsers.tsx/index.tsx'
|
||||
|
||||
export const VerifyPage = () => {
|
||||
const theme = useTheme()
|
||||
@ -67,11 +52,6 @@ export const VerifyPage = () => {
|
||||
|
||||
const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } =
|
||||
useSigitMeta(meta)
|
||||
const profiles = useSigitProfiles([
|
||||
...(submittedBy ? [submittedBy] : []),
|
||||
...signers,
|
||||
...viewers
|
||||
])
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
@ -283,35 +263,6 @@ export const VerifyPage = () => {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const getPrevSignersSig = (npub: string) => {
|
||||
if (!meta) return null
|
||||
|
||||
// if user is first signer then use creator's signature
|
||||
if (signers[0] === npub) {
|
||||
try {
|
||||
const createSignatureEvent: Event = JSON.parse(meta.createSignature)
|
||||
return createSignatureEvent.sig
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// find the index of signer
|
||||
const currentSignerIndex = signers.findIndex((signer) => signer === npub)
|
||||
// return null if could not found user in signer's list
|
||||
if (currentSignerIndex === -1) return null
|
||||
// find prev signer
|
||||
const prevSigner = signers[currentSignerIndex - 1]
|
||||
|
||||
// get the signature of prev signer
|
||||
try {
|
||||
const prevSignersEvent: Event = JSON.parse(meta.docSignatures[prevSigner])
|
||||
return prevSignersEvent.sig
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
|
||||
|
||||
@ -379,76 +330,6 @@ export const VerifyPage = () => {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const displayUser = (pubkey: string, verifySignature = false) => {
|
||||
const profile = profiles[pubkey]
|
||||
|
||||
let isValidSignature = false
|
||||
|
||||
if (verifySignature) {
|
||||
const npub = hexToNpub(pubkey)
|
||||
const signedEventString = meta ? meta.docSignatures[npub] : null
|
||||
if (signedEventString) {
|
||||
try {
|
||||
const signedEvent = JSON.parse(signedEventString)
|
||||
const isVerifiedEvent = verifyEvent(signedEvent)
|
||||
|
||||
if (isVerifiedEvent) {
|
||||
// get the actual signature of prev signer
|
||||
const prevSignersSig = getPrevSignersSig(npub)
|
||||
|
||||
// get the signature of prev signer from the content of current signers signedEvent
|
||||
|
||||
try {
|
||||
const obj: SignedEventContent = JSON.parse(signedEvent.content)
|
||||
if (
|
||||
obj.prevSig &&
|
||||
prevSignersSig &&
|
||||
obj.prevSig === prevSignersSig
|
||||
) {
|
||||
isValidSignature = true
|
||||
}
|
||||
} catch (error) {
|
||||
isValidSignature = false
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`An error occurred in parsing and verifying the signature event for ${pubkey}`,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserAvatar
|
||||
pubkey={pubkey}
|
||||
name={
|
||||
profile?.display_name || profile?.name || shorten(hexToNpub(pubkey))
|
||||
}
|
||||
image={profile?.picture}
|
||||
/>
|
||||
|
||||
{verifySignature && (
|
||||
<>
|
||||
{isValidSignature && (
|
||||
<Tooltip title="Valid signature">
|
||||
<CheckCircle sx={{ color: theme.palette.success.light }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!isValidSignature && (
|
||||
<Tooltip title="Invalid signature">
|
||||
<Cancel sx={{ color: theme.palette.error.main }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const displayExportedBy = () => {
|
||||
if (!meta || !meta.exportSignature) return null
|
||||
|
||||
@ -458,7 +339,7 @@ export const VerifyPage = () => {
|
||||
const exportSignatureEvent = JSON.parse(exportSignatureString) as Event
|
||||
|
||||
if (verifyEvent(exportSignatureEvent)) {
|
||||
return displayUser(exportSignatureEvent.pubkey)
|
||||
// return displayUser(exportSignatureEvent.pubkey)
|
||||
} else {
|
||||
toast.error(`Invalid export signature!`)
|
||||
return (
|
||||
@ -505,109 +386,9 @@ export const VerifyPage = () => {
|
||||
)}
|
||||
|
||||
{meta && (
|
||||
<Files
|
||||
left={
|
||||
<>
|
||||
<List
|
||||
sx={{
|
||||
bgcolor: 'background.paper',
|
||||
marginTop: 2
|
||||
}}
|
||||
subheader={
|
||||
<ListSubheader className={styles.subHeader}>
|
||||
Meta Info
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
{submittedBy && (
|
||||
<ListItem
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
gap: '15px'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: textColor }}>
|
||||
Submitted By
|
||||
</Typography>
|
||||
{displayUser(submittedBy)}
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
<ListItem
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
gap: '15px'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: textColor }}>
|
||||
Exported By
|
||||
</Typography>
|
||||
{displayExportedBy()}
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleExport} variant="contained">
|
||||
Export Sigit
|
||||
</Button>
|
||||
</Box>
|
||||
</ListItem>
|
||||
|
||||
{signers.length > 0 && (
|
||||
<ListItem
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: textColor }}>
|
||||
Signers
|
||||
</Typography>
|
||||
<ul className={styles.usersList}>
|
||||
{signers.map((signer) => (
|
||||
<li
|
||||
key={signer}
|
||||
style={{
|
||||
color: textColor,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '15px'
|
||||
}}
|
||||
>
|
||||
{displayUser(npubToHex(signer)!, true)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
{viewers.length > 0 && (
|
||||
<ListItem
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: textColor }}>
|
||||
Viewers
|
||||
</Typography>
|
||||
<ul className={styles.usersList}>
|
||||
{viewers.map((viewer) => (
|
||||
<li key={viewer} style={{ color: textColor }}>
|
||||
{displayUser(npubToHex(viewer)!)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
<ListItem
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: textColor }}>
|
||||
Files
|
||||
</Typography>
|
||||
<Box className={styles.filesWrapper}>
|
||||
{Object.entries(currentFileHashes).map(
|
||||
([filename, hash], index) => {
|
||||
@ -643,9 +424,17 @@ export const VerifyPage = () => {
|
||||
}
|
||||
)}
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
{displayExportedBy()}
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleExport} variant="contained">
|
||||
Export Sigit
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
right={<FileUsers meta={meta} />}
|
||||
content={<div style={{ height: '300vh' }}></div>}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
|
@ -62,7 +62,7 @@ function handleError(error: unknown): Error {
|
||||
|
||||
// Reuse common error messages for meta parsing
|
||||
export enum SigitMetaParseErrorType {
|
||||
'PARSE_ERROR_SIGNATURE_EVENT' = 'error occurred in parsing the create signature event',
|
||||
'PARSE_ERROR_EVENT' = 'error occurred in parsing the create signature event',
|
||||
'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content"
|
||||
}
|
||||
|
||||
@ -76,24 +76,19 @@ export interface SigitCardDisplayInfo {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for createSignatureEvent parse that throws custom SigitMetaParseError with cause and context
|
||||
* Wrapper for event parser that throws custom SigitMetaParseError with cause and context
|
||||
* @param raw Raw string for parsing
|
||||
* @returns parsed Event
|
||||
*/
|
||||
export const parseCreateSignatureEvent = async (
|
||||
raw: string
|
||||
): Promise<Event> => {
|
||||
export const parseNostrEvent = async (raw: string): Promise<Event> => {
|
||||
try {
|
||||
const createSignatureEvent = await parseJson<Event>(raw)
|
||||
return createSignatureEvent
|
||||
const event = await parseJson<Event>(raw)
|
||||
return event
|
||||
} catch (error) {
|
||||
throw new SigitMetaParseError(
|
||||
SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT,
|
||||
{
|
||||
throw new SigitMetaParseError(SigitMetaParseErrorType.PARSE_ERROR_EVENT, {
|
||||
cause: handleError(error),
|
||||
context: raw
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,9 +130,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const createSignatureEvent = await parseCreateSignatureEvent(
|
||||
meta.createSignature
|
||||
)
|
||||
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
||||
|
||||
// created_at in nostr events are stored in seconds
|
||||
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
|
||||
@ -147,13 +140,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
||||
)
|
||||
|
||||
const files = Object.keys(createSignatureContent.fileHashes)
|
||||
const extensions = files.reduce((result: string[], file: string) => {
|
||||
const extension = file.split('.').pop()
|
||||
if (extension) {
|
||||
result.push(extension)
|
||||
}
|
||||
return result
|
||||
}, [])
|
||||
const extensions = extractFileExtensions(files)
|
||||
|
||||
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
||||
const isCompletelySigned = createSignatureContent.signers.every((signer) =>
|
||||
@ -179,3 +166,15 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const extractFileExtensions = (fileNames: string[]) => {
|
||||
const extensions = fileNames.reduce((result: string[], file: string) => {
|
||||
const extension = file.split('.').pop()
|
||||
if (extension) {
|
||||
result.push(extension)
|
||||
}
|
||||
return result
|
||||
}, [])
|
||||
|
||||
return extensions
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user