Merge branch 'staging' into issue-171
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
This commit is contained in:
commit
6ba3b6ec89
@ -37,7 +37,7 @@ import { setUserRobotImage } from '../../store/userRobotImage/action'
|
||||
import { Container } from '../Container'
|
||||
import { ButtonIcon } from '../ButtonIcon'
|
||||
|
||||
const metadataController = new MetadataController()
|
||||
const metadataController = MetadataController.getInstance()
|
||||
|
||||
export const AppBar = () => {
|
||||
const navigate = useNavigate()
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Meta } from '../../types'
|
||||
import { SigitCardDisplayInfo, SigitStatus, SignStatus } from '../../utils'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils'
|
||||
import { formatTimestamp, npubToHex } from '../../utils'
|
||||
import { appPublicRoutes, appPrivateRoutes } from '../../routes'
|
||||
import { Button, Divider, Tooltip } from '@mui/material'
|
||||
import { DisplaySigner } from '../DisplaySigner'
|
||||
@ -17,9 +17,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { UserAvatarGroup } from '../UserAvatarGroup'
|
||||
|
||||
import styles from './style.module.scss'
|
||||
import { TooltipChild } from '../TooltipChild'
|
||||
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
||||
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
|
||||
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
||||
import { extractFileExtensions } from '../../utils/file'
|
||||
|
||||
@ -38,12 +36,6 @@ export const DisplaySigit = ({
|
||||
parsedMeta
|
||||
|
||||
const { signersStatus, fileHashes } = useSigitMeta(meta)
|
||||
|
||||
const profiles = useSigitProfiles([
|
||||
...(submittedBy ? [submittedBy] : []),
|
||||
...signers
|
||||
])
|
||||
|
||||
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
|
||||
|
||||
return (
|
||||
@ -63,62 +55,29 @@ export const DisplaySigit = ({
|
||||
)}
|
||||
<p className={`line-clamp-2 ${styles.title}`}>{title}</p>
|
||||
<div className={styles.users}>
|
||||
{submittedBy &&
|
||||
(function () {
|
||||
const profile = profiles[submittedBy]
|
||||
return (
|
||||
<Tooltip
|
||||
key={submittedBy}
|
||||
title={
|
||||
profile?.display_name ||
|
||||
profile?.name ||
|
||||
shorten(hexToNpub(submittedBy))
|
||||
}
|
||||
placement="top"
|
||||
arrow
|
||||
disableInteractive
|
||||
>
|
||||
<TooltipChild>
|
||||
<DisplaySigner
|
||||
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
|
||||
profile={profile}
|
||||
pubkey={submittedBy}
|
||||
/>
|
||||
</TooltipChild>
|
||||
</Tooltip>
|
||||
)
|
||||
})()}
|
||||
{submittedBy && (
|
||||
<DisplaySigner
|
||||
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
|
||||
pubkey={submittedBy}
|
||||
/>
|
||||
)}
|
||||
{submittedBy && signers.length ? (
|
||||
<Divider orientation="vertical" flexItem />
|
||||
) : null}
|
||||
<UserAvatarGroup max={7}>
|
||||
{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[signer]}
|
||||
profile={profile}
|
||||
pubkey={pubkey}
|
||||
/>
|
||||
</TooltipChild>
|
||||
</Tooltip>
|
||||
<DisplaySigner
|
||||
key={pubkey}
|
||||
status={signersStatus[signer]}
|
||||
pubkey={pubkey}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</UserAvatarGroup>
|
||||
</div>
|
||||
<div className={`${styles.details} ${styles.date} ${styles.iconLabel}`}>
|
||||
<div className={`${styles.details} ${styles.iconLabel}`}>
|
||||
<FontAwesomeIcon icon={faCalendar} />
|
||||
{createdAt ? formatTimestamp(createdAt) : null}
|
||||
</div>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Badge } from '@mui/material'
|
||||
import { ProfileMetadata } from '../../types'
|
||||
import styles from './style.module.scss'
|
||||
import { UserAvatar } from '../UserAvatar'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
@ -15,38 +14,33 @@ import { SignStatus } from '../../utils'
|
||||
import { Spinner } from '../Spinner'
|
||||
|
||||
type DisplaySignerProps = {
|
||||
profile: ProfileMetadata
|
||||
pubkey: string
|
||||
status: SignStatus
|
||||
}
|
||||
|
||||
export const DisplaySigner = ({
|
||||
status,
|
||||
profile,
|
||||
pubkey
|
||||
}: DisplaySignerProps) => {
|
||||
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={faEllipsis} />
|
||||
case SignStatus.Invalid:
|
||||
return <FontAwesomeIcon icon={faExclamation} />
|
||||
case SignStatus.Viewer:
|
||||
return <FontAwesomeIcon icon={faEye} />
|
||||
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={faEllipsis} />
|
||||
case SignStatus.Invalid:
|
||||
return <FontAwesomeIcon icon={faExclamation} />
|
||||
case SignStatus.Viewer:
|
||||
return <FontAwesomeIcon icon={faEye} />
|
||||
|
||||
default:
|
||||
return <FontAwesomeIcon icon={faQuestion} />
|
||||
}
|
||||
default:
|
||||
return <FontAwesomeIcon icon={faQuestion} />
|
||||
}
|
||||
}
|
||||
|
||||
export const DisplaySigner = ({ status, pubkey }: DisplaySignerProps) => {
|
||||
return (
|
||||
<Badge
|
||||
className={styles.signer}
|
||||
@ -56,7 +50,7 @@ export const DisplaySigner = ({
|
||||
<div className={styles.statusBadge}>{getStatusIcon(status)}</div>
|
||||
}
|
||||
>
|
||||
<UserAvatar pubkey={pubkey} image={profile?.picture} />
|
||||
<UserAvatar pubkey={pubkey} />
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import styles from './style.module.scss'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
interface Props {
|
||||
desc?: string
|
||||
variant?: 'small' | 'default'
|
||||
}
|
||||
|
||||
export const LoadingSpinner = (props: Props) => {
|
||||
const { desc, variant = 'default' } = props
|
||||
export const LoadingSpinner = (props: PropsWithChildren<Props>) => {
|
||||
const { desc, children, variant = 'default' } = props
|
||||
|
||||
switch (variant) {
|
||||
case 'small':
|
||||
@ -20,16 +22,22 @@ export const LoadingSpinner = (props: Props) => {
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
return createPortal(
|
||||
<div className={styles.loadingSpinnerOverlay}>
|
||||
<div
|
||||
className={styles.loadingSpinnerContainer}
|
||||
data-variant={variant}
|
||||
>
|
||||
<div className={styles.loadingSpinner}></div>
|
||||
{desc && <p className={styles.loadingSpinnerDesc}>{desc}</p>}
|
||||
{desc && (
|
||||
<div className={styles.loadingSpinnerDesc}>
|
||||
{desc}
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.getElementById('root')!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -42,11 +42,15 @@
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
border-top: solid 1px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
|
@ -3,34 +3,56 @@ import { getProfileRoute } from '../../routes'
|
||||
import styles from './styles.module.scss'
|
||||
import { AvatarIconButton } from '../UserAvatarIconButton'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useProfileMetadata } from '../../hooks/useProfileMetadata'
|
||||
import { Tooltip } from '@mui/material'
|
||||
import { shorten } from '../../utils'
|
||||
import { TooltipChild } from '../TooltipChild'
|
||||
|
||||
interface UserAvatarProps {
|
||||
name?: string
|
||||
pubkey: string
|
||||
image?: string
|
||||
isNameVisible?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* This component will be used for the displaying username and profile picture.
|
||||
* Clicking will navigate to the user's profile.
|
||||
*/
|
||||
export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => {
|
||||
export const UserAvatar = ({
|
||||
pubkey,
|
||||
isNameVisible = false
|
||||
}: UserAvatarProps) => {
|
||||
const profile = useProfileMetadata(pubkey)
|
||||
const name = profile?.display_name || profile?.name || shorten(pubkey)
|
||||
const image = profile?.picture
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={getProfileRoute(pubkey)}
|
||||
className={styles.container}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<AvatarIconButton
|
||||
src={image}
|
||||
hexKey={pubkey}
|
||||
aria-label={`account of user ${name || pubkey}`}
|
||||
color="inherit"
|
||||
sx={{
|
||||
padding: 0
|
||||
}}
|
||||
/>
|
||||
{name ? <span className={styles.username}>{name}</span> : null}
|
||||
<Tooltip
|
||||
key={pubkey}
|
||||
title={name}
|
||||
placement="top"
|
||||
arrow
|
||||
disableInteractive
|
||||
>
|
||||
<TooltipChild>
|
||||
<AvatarIconButton
|
||||
src={image}
|
||||
hexKey={pubkey}
|
||||
aria-label={`account of user ${name}`}
|
||||
color="inherit"
|
||||
sx={{
|
||||
padding: 0
|
||||
}}
|
||||
/>
|
||||
</TooltipChild>
|
||||
</Tooltip>
|
||||
{isNameVisible && name ? (
|
||||
<span className={styles.username}>{name}</span>
|
||||
) : null}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { Divider, Tooltip } from '@mui/material'
|
||||
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
|
||||
import {
|
||||
formatTimestamp,
|
||||
fromUnixTimestamp,
|
||||
hexToNpub,
|
||||
npubToHex,
|
||||
shorten,
|
||||
SignStatus
|
||||
} from '../../utils'
|
||||
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
||||
@ -24,10 +22,10 @@ import {
|
||||
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { State } from '../../store/rootReducer'
|
||||
import { TooltipChild } from '../TooltipChild'
|
||||
import { DisplaySigner } from '../DisplaySigner'
|
||||
import { Meta } from '../../types'
|
||||
import { extractFileExtensions } from '../../utils/file'
|
||||
import { UserAvatar } from '../UserAvatar'
|
||||
|
||||
interface UsersDetailsProps {
|
||||
meta: Meta
|
||||
@ -36,6 +34,7 @@ interface UsersDetailsProps {
|
||||
export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
const {
|
||||
submittedBy,
|
||||
exportedBy,
|
||||
signers,
|
||||
viewers,
|
||||
fileHashes,
|
||||
@ -47,11 +46,6 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
isValid
|
||||
} = useSigitMeta(meta)
|
||||
const { usersPubkey } = useSelector((state: State) => state.auth)
|
||||
const profiles = useSigitProfiles([
|
||||
...(submittedBy ? [submittedBy] : []),
|
||||
...signers,
|
||||
...viewers
|
||||
])
|
||||
const userCanSign =
|
||||
typeof usersPubkey !== 'undefined' &&
|
||||
signers.includes(hexToNpub(usersPubkey))
|
||||
@ -63,31 +57,12 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
<div className={styles.section}>
|
||||
<p>Signers</p>
|
||||
<div className={styles.users}>
|
||||
{submittedBy &&
|
||||
(function () {
|
||||
const profile = profiles[submittedBy]
|
||||
return (
|
||||
<Tooltip
|
||||
key={submittedBy}
|
||||
title={
|
||||
profile?.display_name ||
|
||||
profile?.name ||
|
||||
shorten(hexToNpub(submittedBy))
|
||||
}
|
||||
placement="top"
|
||||
arrow
|
||||
disableInteractive
|
||||
>
|
||||
<TooltipChild>
|
||||
<DisplaySigner
|
||||
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
|
||||
profile={profile}
|
||||
pubkey={submittedBy}
|
||||
/>
|
||||
</TooltipChild>
|
||||
</Tooltip>
|
||||
)
|
||||
})()}
|
||||
{submittedBy && (
|
||||
<DisplaySigner
|
||||
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
|
||||
pubkey={submittedBy}
|
||||
/>
|
||||
)}
|
||||
|
||||
{submittedBy && signers.length ? (
|
||||
<Divider orientation="vertical" flexItem />
|
||||
@ -96,26 +71,12 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
<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[signer]}
|
||||
profile={profile}
|
||||
pubkey={pubkey}
|
||||
/>
|
||||
</TooltipChild>
|
||||
</Tooltip>
|
||||
<DisplaySigner
|
||||
key={pubkey}
|
||||
status={signersStatus[signer]}
|
||||
pubkey={pubkey}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</UserAvatarGroup>
|
||||
@ -128,34 +89,28 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
<UserAvatarGroup max={20}>
|
||||
{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>
|
||||
<DisplaySigner
|
||||
key={pubkey}
|
||||
status={SignStatus.Viewer}
|
||||
pubkey={pubkey}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</UserAvatarGroup>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{exportedBy && (
|
||||
<>
|
||||
<p>Exported By</p>
|
||||
<div className={styles.users}>
|
||||
<UserAvatar pubkey={exportedBy} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<p>Details</p>
|
||||
|
@ -25,7 +25,7 @@ export class AuthController {
|
||||
|
||||
constructor() {
|
||||
this.nostrController = NostrController.getInstance()
|
||||
this.metadataController = new MetadataController()
|
||||
this.metadataController = MetadataController.getInstance()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
import { DEFAULT_LOOK_UP_RELAY_LIST } from '../utils/const'
|
||||
|
||||
export class MetadataController extends EventEmitter {
|
||||
private static instance: MetadataController
|
||||
private nostrController: NostrController
|
||||
private specialMetadataRelay = 'wss://purplepag.es'
|
||||
private pendingFetches = new Map<string, Promise<Event | null>>() // Track pending fetches
|
||||
@ -31,6 +32,13 @@ export class MetadataController extends EventEmitter {
|
||||
this.nostrController = NostrController.getInstance()
|
||||
}
|
||||
|
||||
public static getInstance(): MetadataController {
|
||||
if (!MetadataController.instance) {
|
||||
MetadataController.instance = new MetadataController()
|
||||
}
|
||||
return MetadataController.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously checks for more recent metadata events authored by a specific key.
|
||||
* If a more recent metadata event is found, it is handled and returned.
|
||||
@ -119,7 +127,6 @@ export class MetadataController extends EventEmitter {
|
||||
// Check if the cached metadata is older than one day
|
||||
if (isOlderThanOneDay(cachedMetadataEvent.cachedAt)) {
|
||||
// If older than one week, find the metadata from relays in background
|
||||
|
||||
this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event)
|
||||
}
|
||||
|
||||
|
46
src/hooks/useProfileMetadata.tsx
Normal file
46
src/hooks/useProfileMetadata.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ProfileMetadata } from '../types/profile'
|
||||
import { MetadataController } from '../controllers/MetadataController'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
|
||||
export const useProfileMetadata = (pubkey: string) => {
|
||||
const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>()
|
||||
|
||||
useEffect(() => {
|
||||
const metadataController = MetadataController.getInstance()
|
||||
const handleMetadataEvent = (event: Event) => {
|
||||
const metadataContent =
|
||||
metadataController.extractProfileMetadataContent(event)
|
||||
|
||||
if (metadataContent) {
|
||||
setProfileMetadata(metadataContent)
|
||||
}
|
||||
}
|
||||
|
||||
if (pubkey) {
|
||||
metadataController.on(pubkey, (kind: number, event: Event) => {
|
||||
if (kind === kinds.Metadata) {
|
||||
handleMetadataEvent(event)
|
||||
}
|
||||
})
|
||||
|
||||
metadataController
|
||||
.findMetadata(pubkey)
|
||||
.then((metadataEvent) => {
|
||||
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
`error occurred in finding metadata for: ${pubkey}`,
|
||||
err
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
metadataController.off(pubkey, handleMetadataEvent)
|
||||
}
|
||||
}, [pubkey])
|
||||
|
||||
return profileMetadata
|
||||
}
|
@ -33,6 +33,10 @@ export interface FlatMeta
|
||||
// Remove pubkey and use submittedBy as `npub1${string}`
|
||||
submittedBy?: `npub1${string}`
|
||||
|
||||
// Optional field only present on exported sigits
|
||||
// Exporting adds user's pubkey
|
||||
exportedBy?: `npub1${string}`
|
||||
|
||||
// Remove created_at and replace with createdAt
|
||||
createdAt?: number
|
||||
|
||||
@ -68,6 +72,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
const [tags, setTags] = useState<string[][]>()
|
||||
const [createdAt, setCreatedAt] = useState<number>()
|
||||
const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event
|
||||
const [exportedBy, setExportedBy] = useState<`npub1${string}`>() // pubkey from export signature nostr event
|
||||
const [id, setId] = useState<string>()
|
||||
const [sig, setSig] = useState<string>()
|
||||
|
||||
@ -99,6 +104,18 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
if (!meta) return
|
||||
;(async function () {
|
||||
try {
|
||||
if (meta.exportSignature) {
|
||||
const exportSignatureEvent = await parseNostrEvent(
|
||||
meta.exportSignature
|
||||
)
|
||||
if (
|
||||
verifyEvent(exportSignatureEvent) &&
|
||||
exportSignatureEvent.pubkey
|
||||
) {
|
||||
setExportedBy(exportSignatureEvent.pubkey as `npub1${string}`)
|
||||
}
|
||||
}
|
||||
|
||||
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
||||
|
||||
const { kind, tags, created_at, pubkey, id, sig, content } =
|
||||
@ -265,6 +282,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
tags,
|
||||
createdAt,
|
||||
submittedBy,
|
||||
exportedBy,
|
||||
id,
|
||||
sig,
|
||||
signers,
|
||||
|
@ -1,71 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ProfileMetadata } from '../types'
|
||||
import { MetadataController } from '../controllers'
|
||||
import { npubToHex } from '../utils'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
|
||||
/**
|
||||
* Extracts profiles from metadata events
|
||||
* @param pubkeys Array of npubs to check
|
||||
* @returns ProfileMetadata
|
||||
*/
|
||||
export const useSigitProfiles = (
|
||||
pubkeys: `npub1${string}`[]
|
||||
): { [key: string]: ProfileMetadata } => {
|
||||
const [profileMetadata, setProfileMetadata] = useState<{
|
||||
[key: string]: ProfileMetadata
|
||||
}>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (pubkeys.length) {
|
||||
const metadataController = new MetadataController()
|
||||
|
||||
// Remove duplicate keys
|
||||
const users = new Set<string>([...pubkeys])
|
||||
|
||||
const handleMetadataEvent = (key: string) => (event: Event) => {
|
||||
const metadataContent =
|
||||
metadataController.extractProfileMetadataContent(event)
|
||||
|
||||
if (metadataContent) {
|
||||
setProfileMetadata((prev) => ({
|
||||
...prev,
|
||||
[key]: metadataContent
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
users.forEach((user) => {
|
||||
const hexKey = npubToHex(user)
|
||||
if (hexKey && !(hexKey in profileMetadata)) {
|
||||
metadataController.on(hexKey, (kind: number, event: Event) => {
|
||||
if (kind === kinds.Metadata) {
|
||||
handleMetadataEvent(hexKey)(event)
|
||||
}
|
||||
})
|
||||
|
||||
metadataController
|
||||
.findMetadata(hexKey)
|
||||
.then((metadataEvent) => {
|
||||
if (metadataEvent) handleMetadataEvent(hexKey)(metadataEvent)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
`error occurred in finding metadata for: ${user}`,
|
||||
err
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
users.forEach((key) => {
|
||||
metadataController.off(key, handleMetadataEvent(key))
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pubkeys])
|
||||
|
||||
return profileMetadata
|
||||
}
|
@ -38,7 +38,7 @@ export const MainLayout = () => {
|
||||
const hasSubscribed = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const metadataController = new MetadataController()
|
||||
const metadataController = MetadataController.getInstance()
|
||||
|
||||
const logout = () => {
|
||||
dispatch(
|
||||
|
@ -36,7 +36,6 @@ import {
|
||||
npubToHex,
|
||||
queryNip05,
|
||||
sendNotification,
|
||||
shorten,
|
||||
signEventForMetaFile,
|
||||
updateUsersAppData,
|
||||
uploadToFileStorage
|
||||
@ -113,6 +112,8 @@ export const CreatePage = () => {
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const signers = users.filter((u) => u.role === UserRole.signer)
|
||||
const viewers = users.filter((u) => u.role === UserRole.viewer)
|
||||
|
||||
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||
|
||||
@ -252,7 +253,7 @@ export const CreatePage = () => {
|
||||
useEffect(() => {
|
||||
users.forEach((user) => {
|
||||
if (!(user.pubkey in metadata)) {
|
||||
const metadataController = new MetadataController()
|
||||
const metadataController = MetadataController.getInstance()
|
||||
|
||||
const handleMetadataEvent = (event: Event) => {
|
||||
const metadataContent =
|
||||
@ -647,6 +648,11 @@ export const CreatePage = () => {
|
||||
}
|
||||
|
||||
saveAs(finalZipFile, `request-${unixNow()}.sigit.zip`)
|
||||
|
||||
// If user is the next signer, we can navigate directly to sign page
|
||||
if (signers[0].pubkey === usersPubkey) {
|
||||
navigate(appPrivateRoutes.sign, { state: { uploadedZip: finalZipFile } })
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
@ -672,9 +678,6 @@ export const CreatePage = () => {
|
||||
},
|
||||
zipUrl: string
|
||||
) => {
|
||||
const signers = users.filter((user) => user.role === UserRole.signer)
|
||||
const viewers = users.filter((user) => user.role === UserRole.viewer)
|
||||
|
||||
const content: CreateSignatureEventContent = {
|
||||
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
|
||||
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
|
||||
@ -703,9 +706,6 @@ export const CreatePage = () => {
|
||||
|
||||
// Send notifications to signers and viewers
|
||||
const sendNotifications = (meta: Meta) => {
|
||||
const signers = users.filter((user) => user.role === UserRole.signer)
|
||||
const viewers = users.filter((user) => user.role === UserRole.viewer)
|
||||
|
||||
// no need to send notification to self so remove it from the list
|
||||
const receivers = (
|
||||
signers.length > 0
|
||||
@ -787,7 +787,7 @@ export const CreatePage = () => {
|
||||
toast.error('Failed to publish notifications')
|
||||
})
|
||||
|
||||
navigate(appPrivateRoutes.sign, { state: { meta: meta } })
|
||||
navigate(appPrivateRoutes.sign, { state: { meta } })
|
||||
} else {
|
||||
const zip = new JSZip()
|
||||
|
||||
@ -914,7 +914,6 @@ export const CreatePage = () => {
|
||||
<div className={styles.flexWrap}>
|
||||
<div className={`${styles.paperGroup} ${styles.users}`}>
|
||||
<DisplayUser
|
||||
metadata={metadata}
|
||||
users={users}
|
||||
handleUserRoleChange={handleUserRoleChange}
|
||||
handleRemoveUser={handleRemoveUser}
|
||||
@ -958,9 +957,6 @@ export const CreatePage = () => {
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={handleCreate} variant="contained">
|
||||
Publish
|
||||
</Button>
|
||||
|
||||
<div className={`${styles.paperGroup} ${styles.toolbox}`}>
|
||||
{toolbox.map((drawTool: DrawTool, index: number) => {
|
||||
@ -987,6 +983,10 @@ export const CreatePage = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button onClick={handleCreate} variant="contained">
|
||||
Publish
|
||||
</Button>
|
||||
|
||||
{!!error && (
|
||||
<FormHelperText error={!!error}>{error}</FormHelperText>
|
||||
)}
|
||||
@ -1010,7 +1010,6 @@ export const CreatePage = () => {
|
||||
}
|
||||
|
||||
type DisplayUsersProps = {
|
||||
metadata: { [key: string]: ProfileMetadata }
|
||||
users: User[]
|
||||
handleUserRoleChange: (role: UserRole, pubkey: string) => void
|
||||
handleRemoveUser: (pubkey: string) => void
|
||||
@ -1018,7 +1017,6 @@ type DisplayUsersProps = {
|
||||
}
|
||||
|
||||
const DisplayUser = ({
|
||||
metadata,
|
||||
users,
|
||||
handleUserRoleChange,
|
||||
handleRemoveUser,
|
||||
@ -1032,7 +1030,6 @@ const DisplayUser = ({
|
||||
.map((user, index) => (
|
||||
<SignerCounterpart
|
||||
key={`signer-${user.pubkey}`}
|
||||
userMeta={metadata[user.pubkey]}
|
||||
user={user}
|
||||
index={index}
|
||||
moveSigner={moveSigner}
|
||||
@ -1047,7 +1044,6 @@ const DisplayUser = ({
|
||||
return (
|
||||
<div className={styles.user} key={`viewer-${user.pubkey}`}>
|
||||
<Counterpart
|
||||
userMeta={metadata[user.pubkey]}
|
||||
user={user}
|
||||
handleUserRoleChange={handleUserRoleChange}
|
||||
handleRemoveUser={handleRemoveUser}
|
||||
@ -1066,7 +1062,6 @@ interface DragItem {
|
||||
}
|
||||
|
||||
type CounterpartProps = {
|
||||
userMeta: ProfileMetadata
|
||||
user: User
|
||||
handleUserRoleChange: (role: UserRole, pubkey: string) => void
|
||||
handleRemoveUser: (pubkey: string) => void
|
||||
@ -1078,7 +1073,6 @@ type SignerCounterpartProps = CounterpartProps & {
|
||||
}
|
||||
|
||||
const SignerCounterpart = ({
|
||||
userMeta,
|
||||
user,
|
||||
index,
|
||||
moveSigner,
|
||||
@ -1171,7 +1165,6 @@ const SignerCounterpart = ({
|
||||
<FontAwesomeIcon width={'14px'} fontSize={'14px'} icon={faGripLines} />
|
||||
<Counterpart
|
||||
user={user}
|
||||
userMeta={userMeta}
|
||||
handleRemoveUser={handleRemoveUser}
|
||||
handleUserRoleChange={handleUserRoleChange}
|
||||
/>
|
||||
@ -1180,7 +1173,6 @@ const SignerCounterpart = ({
|
||||
}
|
||||
|
||||
const Counterpart = ({
|
||||
userMeta,
|
||||
user,
|
||||
handleUserRoleChange,
|
||||
handleRemoveUser
|
||||
@ -1188,15 +1180,7 @@ const Counterpart = ({
|
||||
return (
|
||||
<>
|
||||
<div className={styles.avatar}>
|
||||
<UserAvatar
|
||||
pubkey={user.pubkey}
|
||||
name={
|
||||
userMeta?.display_name ||
|
||||
userMeta?.name ||
|
||||
shorten(hexToNpub(user.pubkey))
|
||||
}
|
||||
image={userMeta?.picture}
|
||||
/>
|
||||
<UserAvatar pubkey={user.pubkey} isNameVisible={true} />
|
||||
</div>
|
||||
<Tooltip title="Toggle User Role" arrow disableInteractive>
|
||||
<Button
|
||||
|
@ -18,12 +18,16 @@ import {
|
||||
} from '../../store/actions'
|
||||
import { LoginMethods } from '../../store/auth/types'
|
||||
import { Dispatch } from '../../store/store'
|
||||
import { npubToHex, queryNip05 } from '../../utils'
|
||||
import { npubToHex, queryNip05, timeout } from '../../utils'
|
||||
import { hexToBytes } from '@noble/hashes/utils'
|
||||
import { NIP05_REGEX } from '../../constants'
|
||||
|
||||
import styles from './styles.module.scss'
|
||||
|
||||
import { TimeoutError } from '../../types/errors/TimeoutError'
|
||||
const EXTENSION_LOGIN_DELAY_SECONDS = 5
|
||||
const EXTENSION_LOGIN_TIMEOUT_SECONDS = EXTENSION_LOGIN_DELAY_SECONDS + 55
|
||||
|
||||
export const Nostr = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
@ -31,11 +35,12 @@ export const Nostr = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const authController = new AuthController()
|
||||
const metadataController = new MetadataController()
|
||||
const metadataController = MetadataController.getInstance()
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
const [isExtensionSlow, setIsExtensionSlow] = useState(false)
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [authUrl, setAuthUrl] = useState<string>()
|
||||
|
||||
@ -72,27 +77,43 @@ export const Nostr = () => {
|
||||
}
|
||||
|
||||
const loginWithExtension = async () => {
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
||||
let waitTimeout: number | undefined
|
||||
try {
|
||||
// Wait EXTENSION_LOGIN_DELAY_SECONDS before showing extension delay message
|
||||
waitTimeout = window.setTimeout(() => {
|
||||
setIsExtensionSlow(true)
|
||||
}, EXTENSION_LOGIN_DELAY_SECONDS * 1000)
|
||||
|
||||
nostrController
|
||||
.capturePublicKey()
|
||||
.then(async (pubkey) => {
|
||||
dispatch(updateLoginMethod(LoginMethods.extension))
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
||||
|
||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||
const redirectPath =
|
||||
await authController.authAndGetMetadataAndRelaysMap(pubkey)
|
||||
const pubkey = await nostrController.capturePublicKey()
|
||||
dispatch(updateLoginMethod(LoginMethods.extension))
|
||||
|
||||
if (redirectPath) navigateAfterLogin(redirectPath)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Error capturing public key from nostr extension: ' + err)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
setLoadingSpinnerDesc('')
|
||||
})
|
||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||
const redirectPath = await Promise.race([
|
||||
authController.authAndGetMetadataAndRelaysMap(pubkey),
|
||||
timeout(EXTENSION_LOGIN_TIMEOUT_SECONDS * 1000)
|
||||
])
|
||||
|
||||
if (redirectPath) {
|
||||
navigateAfterLogin(redirectPath)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof TimeoutError) {
|
||||
// Just log the error, no toast, user has already been notified with the loading screen
|
||||
console.error("Extension didn't respond in time")
|
||||
} else {
|
||||
toast.error('Error capturing public key from nostr extension: ' + error)
|
||||
}
|
||||
} finally {
|
||||
// Clear the wait timeout so we don't change the state unnecessarily
|
||||
window.clearTimeout(waitTimeout)
|
||||
|
||||
setIsLoading(false)
|
||||
setLoadingSpinnerDesc('')
|
||||
setIsExtensionSlow(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -354,7 +375,33 @@ export const Nostr = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
{isLoading && (
|
||||
<LoadingSpinner desc={loadingSpinnerDesc}>
|
||||
{isExtensionSlow && (
|
||||
<>
|
||||
<p>
|
||||
Your nostr extension is not responding. Check these
|
||||
alternatives:{' '}
|
||||
<a href="https://github.com/aljazceru/awesome-nostr?tab=readme-ov-file#nip-07-browser-extensions">
|
||||
https://github.com/aljazceru/awesome-nostr
|
||||
</a>
|
||||
</p>
|
||||
<br />
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setLoadingSpinnerDesc('')
|
||||
setIsLoading(false)
|
||||
setIsExtensionSlow(false)
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</LoadingSpinner>
|
||||
)}
|
||||
|
||||
{isNostrExtensionAvailable && (
|
||||
<>
|
||||
|
@ -27,7 +27,7 @@ export const ProfilePage = () => {
|
||||
|
||||
const { npub } = useParams()
|
||||
|
||||
const metadataController = useMemo(() => new MetadataController(), [])
|
||||
const metadataController = useMemo(() => MetadataController.getInstance(), [])
|
||||
|
||||
const [pubkey, setPubkey] = useState<string>()
|
||||
const [nostrJoiningBlock, setNostrJoiningBlock] =
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
useTheme
|
||||
} from '@mui/material'
|
||||
import { UnsignedEvent, nip19, kinds, VerifiedEvent, Event } from 'nostr-tools'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { MetadataController, NostrController } from '../../../controllers'
|
||||
@ -41,7 +41,7 @@ export const ProfileSettingsPage = () => {
|
||||
|
||||
const dispatch: Dispatch = useDispatch()
|
||||
|
||||
const metadataController = useMemo(() => new MetadataController(), [])
|
||||
const metadataController = MetadataController.getInstance()
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
const [pubkey, setPubkey] = useState<string>()
|
||||
|
@ -33,7 +33,8 @@ import {
|
||||
sendNotification,
|
||||
signEventForMetaFile,
|
||||
updateUsersAppData,
|
||||
findOtherUserMarks
|
||||
findOtherUserMarks,
|
||||
timeout
|
||||
} from '../../utils'
|
||||
import { Container } from '../../components/Container'
|
||||
import { DisplayMeta } from './internal/displayMeta'
|
||||
@ -299,17 +300,10 @@ export const SignPage = () => {
|
||||
setAuthUrl(url)
|
||||
})
|
||||
|
||||
// Set up timeout promise to handle encryption timeout
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timeout occurred'))
|
||||
}, 60000) // Timeout duration = 60 seconds
|
||||
})
|
||||
|
||||
// decrypt the encryptionKey, with timeout
|
||||
// decrypt the encryptionKey, with timeout (duration = 60 seconds)
|
||||
const encryptionKey = await Promise.race([
|
||||
nostrController.nip04Decrypt(sender, key),
|
||||
timeoutPromise
|
||||
timeout(60000)
|
||||
])
|
||||
.then((res) => {
|
||||
return res
|
||||
@ -491,20 +485,20 @@ export const SignPage = () => {
|
||||
const fileNames = Object.values(zip.files)
|
||||
.filter((entry) => entry.name.startsWith('files/') && !entry.dir)
|
||||
.map((entry) => entry.name)
|
||||
.map((entry) => entry.replace(/^files\//, ''))
|
||||
|
||||
// generate hashes for all entries in files folder of zipArchive
|
||||
// these hashes can be used to verify the originality of files
|
||||
for (const fileName of fileNames) {
|
||||
for (const zipFilePath of fileNames) {
|
||||
const arrayBuffer = await readContentOfZipEntry(
|
||||
zip,
|
||||
fileName,
|
||||
zipFilePath,
|
||||
'arraybuffer'
|
||||
)
|
||||
|
||||
const fileName = zipFilePath.replace(/^files\//, '')
|
||||
if (arrayBuffer) {
|
||||
files[fileName] = await convertToSigitFile(arrayBuffer, fileName)
|
||||
|
||||
// generate hashes for all entries in files folder of zipArchive
|
||||
// these hashes can be used to verify the originality of files
|
||||
const hash = await getHash(arrayBuffer)
|
||||
if (hash) {
|
||||
fileHashes[fileName] = hash
|
||||
|
@ -32,7 +32,7 @@ import { useState, useEffect } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { UserAvatar } from '../../../components/UserAvatar'
|
||||
import { MetadataController } from '../../../controllers'
|
||||
import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils'
|
||||
import { npubToHex, hexToNpub, parseJson } from '../../../utils'
|
||||
import styles from '../style.module.scss'
|
||||
import { SigitFile } from '../../../utils/file'
|
||||
|
||||
@ -105,7 +105,7 @@ export const DisplayMeta = ({
|
||||
}, [signers, viewers])
|
||||
|
||||
useEffect(() => {
|
||||
const metadataController = new MetadataController()
|
||||
const metadataController = MetadataController.getInstance()
|
||||
|
||||
const hexKeys: string[] = [
|
||||
npubToHex(submittedBy)!,
|
||||
@ -167,20 +167,7 @@ export const DisplayMeta = ({
|
||||
<Typography variant="h6" sx={{ color: textColor }}>
|
||||
Submitted By
|
||||
</Typography>
|
||||
{(function () {
|
||||
const profile = metadata[submittedBy]
|
||||
return (
|
||||
<UserAvatar
|
||||
pubkey={submittedBy}
|
||||
name={
|
||||
profile?.display_name ||
|
||||
profile?.name ||
|
||||
shorten(hexToNpub(submittedBy))
|
||||
}
|
||||
image={profile?.picture}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
<UserAvatar pubkey={submittedBy} isNameVisible={true} />
|
||||
</ListItem>
|
||||
<ListItem
|
||||
sx={{
|
||||
@ -280,14 +267,12 @@ type DisplayUserProps = {
|
||||
const DisplayUser = ({
|
||||
meta,
|
||||
user,
|
||||
metadata,
|
||||
signedBy,
|
||||
nextSigner,
|
||||
getPrevSignersSig
|
||||
}: DisplayUserProps) => {
|
||||
const theme = useTheme()
|
||||
|
||||
const userMeta = metadata[user.pubkey]
|
||||
const [userStatus, setUserStatus] = useState<UserStatus>(UserStatus.Pending)
|
||||
const [prevSignatureStatus, setPreviousSignatureStatus] =
|
||||
useState<PrevSignatureValidationEnum>(PrevSignatureValidationEnum.Pending)
|
||||
@ -370,15 +355,7 @@ const DisplayUser = ({
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell className={styles.tableCell}>
|
||||
<UserAvatar
|
||||
pubkey={user.pubkey}
|
||||
name={
|
||||
userMeta?.display_name ||
|
||||
userMeta?.name ||
|
||||
shorten(hexToNpub(user.pubkey))
|
||||
}
|
||||
image={userMeta?.picture}
|
||||
/>
|
||||
<UserAvatar pubkey={user.pubkey} isNameVisible={true} />
|
||||
</TableCell>
|
||||
<TableCell className={styles.tableCell}>{user.role}</TableCell>
|
||||
<TableCell>
|
||||
|
@ -1,16 +1,11 @@
|
||||
import { Box, Button, Tooltip, Typography } from '@mui/material'
|
||||
import { Box, Button, Typography } from '@mui/material'
|
||||
import JSZip from 'jszip'
|
||||
import { MuiFileInput } from 'mui-file-input'
|
||||
import { Event, verifyEvent } from 'nostr-tools'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { NostrController } from '../../controllers'
|
||||
import {
|
||||
CreateSignatureEventContent,
|
||||
DocSignatureEvent,
|
||||
Meta
|
||||
} from '../../types'
|
||||
import { DocSignatureEvent, Meta } from '../../types'
|
||||
import {
|
||||
decryptArrayBuffer,
|
||||
extractMarksFromSignedMeta,
|
||||
@ -20,7 +15,6 @@ import {
|
||||
parseJson,
|
||||
readContentOfZipEntry,
|
||||
signEventForMetaFile,
|
||||
shorten,
|
||||
getCurrentUserFiles
|
||||
} from '../../utils'
|
||||
import styles from './style.module.scss'
|
||||
@ -42,9 +36,6 @@ import { Container } from '../../components/Container'
|
||||
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
||||
import { StickySideColumns } from '../../layouts/StickySideColumns.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'
|
||||
import FileList from '../../components/FileList'
|
||||
import { CurrentUserFile } from '../../types/file.ts'
|
||||
import { Mark } from '../../types/mark.ts'
|
||||
@ -163,12 +154,26 @@ const SlimPdfView = ({
|
||||
|
||||
export const VerifyPage = () => {
|
||||
const location = useLocation()
|
||||
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
|
||||
/**
|
||||
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json
|
||||
* meta will be received in navigation from create & home page in online mode
|
||||
*/
|
||||
const { uploadedZip, meta } = location.state || {}
|
||||
const { uploadedZip, meta: metaInNavState } = location.state || {}
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
useEffect(() => {
|
||||
if (uploadedZip) {
|
||||
setSelectedFile(uploadedZip)
|
||||
}
|
||||
}, [uploadedZip])
|
||||
|
||||
const [meta, setMeta] = useState<Meta>(metaInNavState)
|
||||
const {
|
||||
submittedBy,
|
||||
zipUrl,
|
||||
@ -179,44 +184,22 @@ export const VerifyPage = () => {
|
||||
parsedSignatureEvents
|
||||
} = useSigitMeta(meta)
|
||||
|
||||
const profiles = useSigitProfiles([
|
||||
...(submittedBy ? [submittedBy] : []),
|
||||
...signers,
|
||||
...viewers
|
||||
])
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
||||
|
||||
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
|
||||
const [currentFileHashes, setCurrentFileHashes] = useState<{
|
||||
[key: string]: string | null
|
||||
}>({})
|
||||
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
||||
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
|
||||
const [signatureFileHashes, setSignatureFileHashes] = useState<{
|
||||
[key: string]: string
|
||||
}>(fileHashes)
|
||||
|
||||
useEffect(() => {
|
||||
setSignatureFileHashes(fileHashes)
|
||||
}, [fileHashes])
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.entries(files).length > 0) {
|
||||
const tmp = getCurrentUserFiles(files, fileHashes, signatureFileHashes)
|
||||
const tmp = getCurrentUserFiles(files, currentFileHashes, fileHashes)
|
||||
setCurrentFile(tmp[0])
|
||||
}
|
||||
}, [signatureFileHashes, fileHashes, files])
|
||||
|
||||
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||
const nostrController = NostrController.getInstance()
|
||||
}, [currentFileHashes, fileHashes, files])
|
||||
|
||||
useEffect(() => {
|
||||
if (uploadedZip) {
|
||||
setSelectedFile(uploadedZip)
|
||||
} else if (meta && encryptionKey) {
|
||||
if (metaInNavState && encryptionKey) {
|
||||
const processSigit = async () => {
|
||||
setIsLoading(true)
|
||||
|
||||
@ -301,7 +284,7 @@ export const VerifyPage = () => {
|
||||
|
||||
processSigit()
|
||||
}
|
||||
}, [encryptionKey, meta, uploadedZip, zipUrl])
|
||||
}, [encryptionKey, metaInNavState, zipUrl])
|
||||
|
||||
const handleVerify = async () => {
|
||||
if (!selectedFile) return
|
||||
@ -315,6 +298,7 @@ export const VerifyPage = () => {
|
||||
|
||||
if (!zip) return
|
||||
|
||||
const files: { [filename: string]: SigitFile } = {}
|
||||
const fileHashes: { [key: string]: string | null } = {}
|
||||
const fileNames = Object.values(zip.files)
|
||||
.filter((entry) => entry.name.startsWith('files/') && !entry.dir)
|
||||
@ -322,24 +306,27 @@ export const VerifyPage = () => {
|
||||
|
||||
// generate hashes for all entries in files folder of zipArchive
|
||||
// these hashes can be used to verify the originality of files
|
||||
for (const fileName of fileNames) {
|
||||
for (const zipFilePath of fileNames) {
|
||||
const arrayBuffer = await readContentOfZipEntry(
|
||||
zip,
|
||||
fileName,
|
||||
zipFilePath,
|
||||
'arraybuffer'
|
||||
)
|
||||
|
||||
const fileName = zipFilePath.replace(/^files\//, '')
|
||||
if (arrayBuffer) {
|
||||
files[fileName] = await convertToSigitFile(arrayBuffer, fileName)
|
||||
const hash = await getHash(arrayBuffer)
|
||||
|
||||
if (hash) {
|
||||
fileHashes[fileName.replace(/^files\//, '')] = hash
|
||||
fileHashes[fileName] = hash
|
||||
}
|
||||
} else {
|
||||
fileHashes[fileName.replace(/^files\//, '')] = null
|
||||
fileHashes[fileName] = null
|
||||
}
|
||||
}
|
||||
|
||||
setFiles(files)
|
||||
setCurrentFileHashes(fileHashes)
|
||||
|
||||
setLoadingSpinnerDesc('Parsing meta.json')
|
||||
@ -368,43 +355,7 @@ export const VerifyPage = () => {
|
||||
|
||||
if (!parsedMetaJson) return
|
||||
|
||||
const createSignatureEvent = await parseJson<Event>(
|
||||
parsedMetaJson.createSignature
|
||||
).catch((err) => {
|
||||
console.log('err in parsing the createSignature event:>> ', err)
|
||||
toast.error(
|
||||
err.message || 'error occurred in parsing the create signature event'
|
||||
)
|
||||
setIsLoading(false)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!createSignatureEvent) return
|
||||
|
||||
const isValidCreateSignature = verifyEvent(createSignatureEvent)
|
||||
|
||||
if (!isValidCreateSignature) {
|
||||
toast.error('Create signature is invalid')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const createSignatureContent = await parseJson<CreateSignatureEventContent>(
|
||||
createSignatureEvent.content
|
||||
).catch((err) => {
|
||||
console.log(
|
||||
`err in parsing the createSignature event's content :>> `,
|
||||
err
|
||||
)
|
||||
toast.error(
|
||||
err.message ||
|
||||
`error occurred in parsing the create signature event's content`
|
||||
)
|
||||
setIsLoading(false)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!createSignatureContent) return
|
||||
setMeta(parsedMetaJson)
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
@ -479,47 +430,6 @@ export const VerifyPage = () => {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const displayExportedBy = () => {
|
||||
if (!meta || !meta.exportSignature) return null
|
||||
|
||||
const exportSignatureString = meta.exportSignature
|
||||
|
||||
try {
|
||||
const exportSignatureEvent = JSON.parse(exportSignatureString) as Event
|
||||
|
||||
if (verifyEvent(exportSignatureEvent)) {
|
||||
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 {
|
||||
toast.error(`Invalid export signature!`)
|
||||
return (
|
||||
<Typography component="label" sx={{ color: 'red' }}>
|
||||
Invalid export signature
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`An error occurred wile parsing exportSignature`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
@ -554,22 +464,19 @@ export const VerifyPage = () => {
|
||||
{meta && (
|
||||
<StickySideColumns
|
||||
left={
|
||||
<>
|
||||
{currentFile !== null && (
|
||||
<FileList
|
||||
files={getCurrentUserFiles(
|
||||
files,
|
||||
currentFileHashes,
|
||||
signatureFileHashes
|
||||
)}
|
||||
currentFile={currentFile}
|
||||
setCurrentFile={setCurrentFile}
|
||||
handleDownload={handleExport}
|
||||
downloadLabel="Download Sigit"
|
||||
/>
|
||||
)}
|
||||
{displayExportedBy()}
|
||||
</>
|
||||
currentFile !== null && (
|
||||
<FileList
|
||||
files={getCurrentUserFiles(
|
||||
files,
|
||||
currentFileHashes,
|
||||
fileHashes
|
||||
)}
|
||||
currentFile={currentFile}
|
||||
setCurrentFile={setCurrentFile}
|
||||
handleDownload={handleExport}
|
||||
downloadLabel="Download Sigit"
|
||||
/>
|
||||
)
|
||||
}
|
||||
right={<UsersDetails meta={meta} />}
|
||||
leftIcon={faFileDownload}
|
||||
@ -578,11 +485,7 @@ export const VerifyPage = () => {
|
||||
>
|
||||
<SlimPdfView
|
||||
currentFile={currentFile}
|
||||
files={getCurrentUserFiles(
|
||||
files,
|
||||
currentFileHashes,
|
||||
signatureFileHashes
|
||||
)}
|
||||
files={getCurrentUserFiles(files, currentFileHashes, fileHashes)}
|
||||
parsedSignatureEvents={parsedSignatureEvents}
|
||||
/>
|
||||
</StickySideColumns>
|
||||
|
6
src/types/errors/TimeoutError.ts
Normal file
6
src/types/errors/TimeoutError.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export class TimeoutError extends Error {
|
||||
constructor() {
|
||||
super('Timeout')
|
||||
this.name = this.constructor.name
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ import { setRelayInfoAction } from '../store/actions'
|
||||
export const getNostrJoiningBlockNumber = async (
|
||||
hexKey: string
|
||||
): Promise<NostrJoiningBlock | null> => {
|
||||
const metadataController = new MetadataController()
|
||||
const metadataController = MetadataController.getInstance()
|
||||
|
||||
const relaySet = await metadataController.findRelayListMetadata(hexKey)
|
||||
|
||||
|
@ -365,7 +365,7 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
|
||||
// Check if relayMap is undefined in the Redux store
|
||||
if (!relayMap) {
|
||||
// If relayMap is not present, fetch relay list metadata
|
||||
const metadataController = new MetadataController()
|
||||
const metadataController = MetadataController.getInstance()
|
||||
const relaySet = await metadataController
|
||||
.findRelayListMetadata(usersPubkey)
|
||||
.catch((err) => {
|
||||
@ -835,7 +835,7 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
|
||||
*/
|
||||
export const subscribeForSigits = async (pubkey: string) => {
|
||||
// Instantiate the MetadataController to retrieve relay list metadata
|
||||
const metadataController = new MetadataController()
|
||||
const metadataController = MetadataController.getInstance()
|
||||
const relaySet = await metadataController
|
||||
.findRelayListMetadata(pubkey)
|
||||
.catch((err) => {
|
||||
@ -939,7 +939,7 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
|
||||
const wrappedEvent = createWrap(unsignedEvent, receiver)
|
||||
|
||||
// Instantiate the MetadataController to retrieve relay list metadata
|
||||
const metadataController = new MetadataController()
|
||||
const metadataController = MetadataController.getInstance()
|
||||
const relaySet = await metadataController
|
||||
.findRelayListMetadata(receiver)
|
||||
.catch((err) => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { TimeoutError } from '../types/errors/TimeoutError.ts'
|
||||
import { CurrentUserFile } from '../types/file.ts'
|
||||
import { SigitFile } from './file.ts'
|
||||
|
||||
@ -34,7 +35,7 @@ export const isOnline = async () => {
|
||||
|
||||
try {
|
||||
// Define a URL to check the online status
|
||||
const url = 'https://www.google.com'
|
||||
const url = document.location.pathname + '?v=' + new Date().getTime()
|
||||
|
||||
// Make a HEAD request to the URL with 'no-cors' mode
|
||||
// This mode is used to handle opaque responses which do not expose their content
|
||||
@ -63,7 +64,7 @@ export const timeout = (ms: number = 60000) => {
|
||||
// Set a timeout using setTimeout
|
||||
setTimeout(() => {
|
||||
// Reject the promise with an Error indicating a timeout
|
||||
reject(new Error('Timeout'))
|
||||
reject(new TimeoutError())
|
||||
}, ms) // Timeout duration in milliseconds
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user