Compare commits
2 Commits
5ffedb68d6
...
4b04bdf39e
Author | SHA1 | Date | |
---|---|---|---|
4b04bdf39e | |||
01ca81be2a |
@ -20,6 +20,7 @@ import styles from './style.module.scss'
|
|||||||
import { TooltipChild } from '../TooltipChild'
|
import { TooltipChild } from '../TooltipChild'
|
||||||
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
||||||
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
|
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
|
||||||
|
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
||||||
|
|
||||||
type SigitProps = {
|
type SigitProps = {
|
||||||
meta: Meta
|
meta: Meta
|
||||||
@ -36,6 +37,8 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
|
|||||||
fileExtensions
|
fileExtensions
|
||||||
} = parsedMeta
|
} = parsedMeta
|
||||||
|
|
||||||
|
const { signersStatus } = useSigitMeta(meta)
|
||||||
|
|
||||||
const profiles = useSigitProfiles([
|
const profiles = useSigitProfiles([
|
||||||
...(submittedBy ? [submittedBy] : []),
|
...(submittedBy ? [submittedBy] : []),
|
||||||
...signers
|
...signers
|
||||||
@ -94,7 +97,7 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
|
|||||||
>
|
>
|
||||||
<TooltipChild>
|
<TooltipChild>
|
||||||
<DisplaySigner
|
<DisplaySigner
|
||||||
meta={meta}
|
status={signersStatus[signer]}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
pubkey={pubkey}
|
pubkey={pubkey}
|
||||||
/>
|
/>
|
||||||
|
@ -1,75 +1,58 @@
|
|||||||
import { Badge } from '@mui/material'
|
import { Badge } from '@mui/material'
|
||||||
import { Event, verifyEvent } from 'nostr-tools'
|
import { ProfileMetadata } from '../../types'
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { Meta, ProfileMetadata } from '../../types'
|
|
||||||
import { hexToNpub, parseJson } from '../../utils'
|
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { UserAvatar } from '../UserAvatar'
|
import { UserAvatar } from '../UserAvatar'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faCheck, faExclamation } from '@fortawesome/free-solid-svg-icons'
|
import {
|
||||||
|
faCheck,
|
||||||
enum SignStatus {
|
faExclamation,
|
||||||
Signed = 'Signed',
|
faEye,
|
||||||
Pending = 'Pending',
|
faHourglass,
|
||||||
Invalid = 'Invalid Sign'
|
faQuestion
|
||||||
}
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { SignStatus } from '../../utils'
|
||||||
|
import { Spinner } from '../Spinner'
|
||||||
|
|
||||||
type DisplaySignerProps = {
|
type DisplaySignerProps = {
|
||||||
meta: Meta
|
|
||||||
profile: ProfileMetadata
|
profile: ProfileMetadata
|
||||||
pubkey: string
|
pubkey: string
|
||||||
|
status: SignStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DisplaySigner = ({
|
export const DisplaySigner = ({
|
||||||
meta,
|
status,
|
||||||
profile,
|
profile,
|
||||||
pubkey
|
pubkey
|
||||||
}: DisplaySignerProps) => {
|
}: DisplaySignerProps) => {
|
||||||
const [signStatus, setSignedStatus] = useState<SignStatus>()
|
const getStatusIcon = (status: SignStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case SignStatus.Signed:
|
||||||
|
return <FontAwesomeIcon icon={faCheck} />
|
||||||
|
case SignStatus.Awaiting:
|
||||||
|
return (
|
||||||
|
<Spinner>
|
||||||
|
<FontAwesomeIcon icon={faHourglass} />
|
||||||
|
</Spinner>
|
||||||
|
)
|
||||||
|
case SignStatus.Pending:
|
||||||
|
return <FontAwesomeIcon icon={faHourglass} />
|
||||||
|
case SignStatus.Invalid:
|
||||||
|
return <FontAwesomeIcon icon={faExclamation} />
|
||||||
|
case SignStatus.Viewer:
|
||||||
|
return <FontAwesomeIcon icon={faEye} />
|
||||||
|
|
||||||
useEffect(() => {
|
default:
|
||||||
if (!meta) return
|
return <FontAwesomeIcon icon={faQuestion} />
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
className={styles.signer}
|
className={styles.signer}
|
||||||
overlap="circular"
|
overlap="circular"
|
||||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||||
badgeContent={
|
badgeContent={
|
||||||
signStatus !== SignStatus.Pending && (
|
<div className={styles.statusBadge}>{getStatusIcon(status)}</div>
|
||||||
<div className={styles.statusBadge}>
|
|
||||||
{signStatus === SignStatus.Signed && (
|
|
||||||
<FontAwesomeIcon icon={faCheck} />
|
|
||||||
)}
|
|
||||||
{signStatus === SignStatus.Invalid && (
|
|
||||||
<FontAwesomeIcon icon={faExclamation} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<UserAvatar pubkey={pubkey} image={profile?.picture} />
|
<UserAvatar pubkey={pubkey} image={profile?.picture} />
|
||||||
|
@ -1,246 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
6
src/components/Spinner/index.tsx
Normal file
6
src/components/Spinner/index.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
|
||||||
|
export const Spinner = ({ children }: PropsWithChildren) => (
|
||||||
|
<div className={styles.spin}>{children}</div>
|
||||||
|
)
|
12
src/components/Spinner/style.module.scss
Normal file
12
src/components/Spinner/style.module.scss
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
224
src/components/UsersDetails.tsx/index.tsx
Normal file
224
src/components/UsersDetails.tsx/index.tsx
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import { Divider, Tooltip } from '@mui/material'
|
||||||
|
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
|
||||||
|
import {
|
||||||
|
extractFileExtensions,
|
||||||
|
formatTimestamp,
|
||||||
|
fromUnixTimestamp,
|
||||||
|
hexToNpub,
|
||||||
|
npubToHex,
|
||||||
|
shorten,
|
||||||
|
SignStatus
|
||||||
|
} from '../../utils'
|
||||||
|
import { UserAvatar } from '../UserAvatar'
|
||||||
|
import { FlatMeta } 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'
|
||||||
|
import { TooltipChild } from '../TooltipChild'
|
||||||
|
import { DisplaySigner } from '../DisplaySigner'
|
||||||
|
|
||||||
|
type UsersDetailsProps = Pick<
|
||||||
|
FlatMeta,
|
||||||
|
| 'submittedBy'
|
||||||
|
| 'signers'
|
||||||
|
| 'viewers'
|
||||||
|
| 'fileHashes'
|
||||||
|
| 'parsedSignatureEvents'
|
||||||
|
| 'createdAt'
|
||||||
|
| 'signedStatus'
|
||||||
|
| 'completedAt'
|
||||||
|
| 'signersStatus'
|
||||||
|
>
|
||||||
|
|
||||||
|
export const UsersDetails = ({
|
||||||
|
submittedBy,
|
||||||
|
signers,
|
||||||
|
viewers,
|
||||||
|
fileHashes,
|
||||||
|
parsedSignatureEvents,
|
||||||
|
createdAt,
|
||||||
|
signedStatus,
|
||||||
|
completedAt,
|
||||||
|
signersStatus
|
||||||
|
}: UsersDetailsProps) => {
|
||||||
|
const { usersPubkey } = useSelector((state: State) => state.auth)
|
||||||
|
const profiles = useSigitProfiles([
|
||||||
|
...(submittedBy ? [submittedBy] : []),
|
||||||
|
...signers,
|
||||||
|
...viewers
|
||||||
|
])
|
||||||
|
const userCanSign =
|
||||||
|
typeof usersPubkey !== 'undefined' &&
|
||||||
|
signers.includes(hexToNpub(usersPubkey))
|
||||||
|
|
||||||
|
const ext = extractFileExtensions(Object.keys(fileHashes))
|
||||||
|
|
||||||
|
return submittedBy ? (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<p>Signers</p>
|
||||||
|
<div className={styles.users}>
|
||||||
|
{submittedBy &&
|
||||||
|
(function () {
|
||||||
|
const profile = profiles[submittedBy]
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
profile?.display_name ||
|
||||||
|
profile?.name ||
|
||||||
|
shorten(hexToNpub(submittedBy))
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<TooltipChild>
|
||||||
|
<UserAvatar pubkey={submittedBy} image={profile?.picture} />
|
||||||
|
</TooltipChild>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{submittedBy && signers.length ? (
|
||||||
|
<Divider orientation="vertical" flexItem />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<UserAvatarGroup max={20}>
|
||||||
|
{signers.map((signer) => {
|
||||||
|
const pubkey = npubToHex(signer)!
|
||||||
|
const profile = profiles[pubkey]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
key={signer}
|
||||||
|
title={
|
||||||
|
profile?.display_name || profile?.name || shorten(pubkey)
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<TooltipChild>
|
||||||
|
<DisplaySigner
|
||||||
|
status={signersStatus[pubkey as `npub1${string}`]}
|
||||||
|
profile={profile}
|
||||||
|
pubkey={pubkey}
|
||||||
|
/>
|
||||||
|
</TooltipChild>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{viewers.map((signer) => {
|
||||||
|
const pubkey = npubToHex(signer)!
|
||||||
|
const profile = profiles[pubkey]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
key={signer}
|
||||||
|
title={
|
||||||
|
profile?.display_name || profile?.name || shorten(pubkey)
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<TooltipChild>
|
||||||
|
<DisplaySigner
|
||||||
|
status={SignStatus.Viewer}
|
||||||
|
profile={profile}
|
||||||
|
pubkey={pubkey}
|
||||||
|
/>
|
||||||
|
</TooltipChild>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</UserAvatarGroup>
|
||||||
|
</div>
|
||||||
|
</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
|
||||||
|
}
|
@ -17,6 +17,11 @@
|
|||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.users {
|
||||||
|
display: flex;
|
||||||
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.detailsItem {
|
.detailsItem {
|
||||||
transition: ease 0.2s;
|
transition: ease 0.2s;
|
||||||
color: rgba(0, 0, 0, 0.5);
|
color: rgba(0, 0, 0, 0.5);
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { CreateSignatureEventContent, Meta } from '../types'
|
import { CreateSignatureEventContent, Meta, SignedEventContent } from '../types'
|
||||||
import { Mark } from '../types/mark'
|
import { Mark } from '../types/mark'
|
||||||
import {
|
import {
|
||||||
fromUnixTimestamp,
|
fromUnixTimestamp,
|
||||||
@ -118,7 +118,6 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
|
|
||||||
if (meta.keys) {
|
if (meta.keys) {
|
||||||
const { sender, keys } = meta.keys
|
const { sender, keys } = meta.keys
|
||||||
|
|
||||||
// Retrieve the user's public key from the state
|
// Retrieve the user's public key from the state
|
||||||
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
|
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
|
||||||
const usersNpub = hexToNpub(usersPubkey)
|
const usersNpub = hexToNpub(usersPubkey)
|
||||||
@ -141,8 +140,28 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temp. map to hold events
|
// Temp. map to hold events and signers
|
||||||
const parsedSignatureEventsMap = new Map<`npub1${string}`, Event>()
|
const parsedSignatureEventsMap = new Map<`npub1${string}`, Event>()
|
||||||
|
const signerStatusMap = new Map<`npub1${string}`, SignStatus>()
|
||||||
|
|
||||||
|
const getPrevSignerSig = (npub: `npub1${string}`) => {
|
||||||
|
if (signers[0] === npub) {
|
||||||
|
return sig
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the index of signer
|
||||||
|
const currentSignerIndex = signers.findIndex(
|
||||||
|
(signer) => signer === npub
|
||||||
|
)
|
||||||
|
// return if could not found user in signer's list
|
||||||
|
if (currentSignerIndex === -1) return
|
||||||
|
// find prev signer
|
||||||
|
const prevSigner = signers[currentSignerIndex - 1]
|
||||||
|
|
||||||
|
// get the signature of prev signer
|
||||||
|
return parsedSignatureEventsMap.get(prevSigner)?.sig
|
||||||
|
}
|
||||||
|
|
||||||
for (const npub in meta.docSignatures) {
|
for (const npub in meta.docSignatures) {
|
||||||
try {
|
try {
|
||||||
// Parse each signature event
|
// Parse each signature event
|
||||||
@ -150,35 +169,50 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
meta.docSignatures[npub as `npub1${string}`]
|
meta.docSignatures[npub as `npub1${string}`]
|
||||||
)
|
)
|
||||||
|
|
||||||
const isValidSignature = verifyEvent(event)
|
|
||||||
|
|
||||||
// Save events to a map, to save all at once outside loop
|
// Save events to a map, to save all at once outside loop
|
||||||
// We need the object to find completedAt
|
// We need the object to find completedAt
|
||||||
// Avoided using parsedSignatureEvents due to useEffect deps
|
// Avoided using parsedSignatureEvents due to useEffect deps
|
||||||
parsedSignatureEventsMap.set(npub as `npub1${string}`, event)
|
parsedSignatureEventsMap.set(npub as `npub1${string}`, event)
|
||||||
|
|
||||||
setSignersStatus((prev) => {
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[npub]: isValidSignature
|
|
||||||
? SignStatus.Signed
|
|
||||||
: SignStatus.Invalid
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSignersStatus((prev) => {
|
signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid)
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[npub]: SignStatus.Invalid
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setParsedSignatureEvents(
|
parsedSignatureEventsMap.forEach((event, npub) => {
|
||||||
Object.fromEntries(parsedSignatureEventsMap.entries())
|
const isValidSignature = verifyEvent(event)
|
||||||
|
if (isValidSignature) {
|
||||||
|
// get the signature of prev signer from the content of current signers signedEvent
|
||||||
|
const prevSignersSig = getPrevSignerSig(npub)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const obj: SignedEventContent = JSON.parse(event.content)
|
||||||
|
if (
|
||||||
|
obj.prevSig &&
|
||||||
|
prevSignersSig &&
|
||||||
|
obj.prevSig === prevSignersSig
|
||||||
|
) {
|
||||||
|
signerStatusMap.set(npub as `npub1${string}`, SignStatus.Signed)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const nextSigner = signers.find((s) => !parsedSignatureEventsMap.has(s))
|
||||||
|
if (nextSigner) {
|
||||||
|
signerStatusMap.set(nextSigner, SignStatus.Awaiting)
|
||||||
|
}
|
||||||
|
|
||||||
|
signers
|
||||||
|
.filter((s) => !(s in meta.docSignatures))
|
||||||
|
.forEach((s) =>
|
||||||
|
signerStatusMap.set(s as `npub1${string}`, SignStatus.Pending)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
setSignersStatus(Object.fromEntries(signerStatusMap))
|
||||||
|
setParsedSignatureEvents(Object.fromEntries(parsedSignatureEventsMap))
|
||||||
|
|
||||||
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
||||||
const isCompletelySigned = signers.every((signer) =>
|
const isCompletelySigned = signers.every((signer) =>
|
||||||
signedBy.includes(signer)
|
signedBy.includes(signer)
|
||||||
@ -187,7 +221,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial
|
isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if all signers signed and are valid
|
// Check if all signers signed
|
||||||
if (isCompletelySigned) {
|
if (isCompletelySigned) {
|
||||||
setCompletedAt(
|
setCompletedAt(
|
||||||
fromUnixTimestamp(
|
fromUnixTimestamp(
|
||||||
|
@ -15,7 +15,8 @@ import {
|
|||||||
unixNow,
|
unixNow,
|
||||||
parseJson,
|
parseJson,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
signEventForMetaFile
|
signEventForMetaFile,
|
||||||
|
shorten
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { Cancel, CheckCircle } from '@mui/icons-material'
|
import { Cancel, CheckCircle } from '@mui/icons-material'
|
||||||
@ -34,8 +35,11 @@ import { getLastSignersSig } from '../../utils/sign.ts'
|
|||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
||||||
import { Files } from '../../layouts/Files.tsx'
|
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
||||||
import { FileUsers } from '../../components/FilesUsers.tsx/index.tsx'
|
import { UsersDetails } from '../../components/UsersDetails.tsx/index.tsx'
|
||||||
|
import { UserAvatar } from '../../components/UserAvatar/index.tsx'
|
||||||
|
import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx'
|
||||||
|
import { TooltipChild } from '../../components/TooltipChild.tsx'
|
||||||
|
|
||||||
export const VerifyPage = () => {
|
export const VerifyPage = () => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@ -50,8 +54,25 @@ export const VerifyPage = () => {
|
|||||||
*/
|
*/
|
||||||
const { uploadedZip, meta } = location.state || {}
|
const { uploadedZip, meta } = location.state || {}
|
||||||
|
|
||||||
const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } =
|
const {
|
||||||
useSigitMeta(meta)
|
submittedBy,
|
||||||
|
zipUrl,
|
||||||
|
encryptionKey,
|
||||||
|
signers,
|
||||||
|
viewers,
|
||||||
|
fileHashes,
|
||||||
|
parsedSignatureEvents,
|
||||||
|
createdAt,
|
||||||
|
signedStatus,
|
||||||
|
completedAt,
|
||||||
|
signersStatus
|
||||||
|
} = useSigitMeta(meta)
|
||||||
|
|
||||||
|
const profiles = useSigitProfiles([
|
||||||
|
...(submittedBy ? [submittedBy] : []),
|
||||||
|
...signers,
|
||||||
|
...viewers
|
||||||
|
])
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
@ -339,7 +360,24 @@ export const VerifyPage = () => {
|
|||||||
const exportSignatureEvent = JSON.parse(exportSignatureString) as Event
|
const exportSignatureEvent = JSON.parse(exportSignatureString) as Event
|
||||||
|
|
||||||
if (verifyEvent(exportSignatureEvent)) {
|
if (verifyEvent(exportSignatureEvent)) {
|
||||||
// return displayUser(exportSignatureEvent.pubkey)
|
const exportedBy = exportSignatureEvent.pubkey
|
||||||
|
const profile = profiles[exportedBy]
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
profile?.display_name ||
|
||||||
|
profile?.name ||
|
||||||
|
shorten(hexToNpub(exportedBy))
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<TooltipChild>
|
||||||
|
<UserAvatar pubkey={exportedBy} image={profile?.picture} />
|
||||||
|
</TooltipChild>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Invalid export signature!`)
|
toast.error(`Invalid export signature!`)
|
||||||
return (
|
return (
|
||||||
@ -386,7 +424,7 @@ export const VerifyPage = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{meta && (
|
{meta && (
|
||||||
<Files
|
<StickySideColumns
|
||||||
left={
|
left={
|
||||||
<>
|
<>
|
||||||
<Box className={styles.filesWrapper}>
|
<Box className={styles.filesWrapper}>
|
||||||
@ -432,8 +470,19 @@ export const VerifyPage = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
right={<FileUsers meta={meta} />}
|
right={
|
||||||
content={<div style={{ height: '300vh' }}></div>}
|
<UsersDetails
|
||||||
|
submittedBy={submittedBy}
|
||||||
|
signers={signers}
|
||||||
|
viewers={viewers}
|
||||||
|
fileHashes={fileHashes}
|
||||||
|
parsedSignatureEvents={parsedSignatureEvents}
|
||||||
|
createdAt={createdAt}
|
||||||
|
signedStatus={signedStatus}
|
||||||
|
completedAt={completedAt}
|
||||||
|
signersStatus={signersStatus}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -5,8 +5,10 @@ import { toast } from 'react-toastify'
|
|||||||
|
|
||||||
export enum SignStatus {
|
export enum SignStatus {
|
||||||
Signed = 'Signed',
|
Signed = 'Signed',
|
||||||
|
Awaiting = 'Awaiting',
|
||||||
Pending = 'Pending',
|
Pending = 'Pending',
|
||||||
Invalid = 'Invalid Sign'
|
Invalid = 'Invalid',
|
||||||
|
Viewer = 'Viewer'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SigitStatus {
|
export enum SigitStatus {
|
||||||
|
Loading…
Reference in New Issue
Block a user