Compare commits
No commits in common. "dd97dfbaf0beedf31d82ef42f70209f379566df7" and "675a763af3562330ec1a3cbb4ad780490d5ca6de" have entirely different histories.
dd97dfbaf0
...
675a763af3
10
package-lock.json
generated
10
package-lock.json
generated
@ -21,7 +21,6 @@
|
|||||||
"@mui/material": "5.15.11",
|
"@mui/material": "5.15.11",
|
||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@nostr-dev-kit/ndk": "2.5.0",
|
"@nostr-dev-kit/ndk": "2.5.0",
|
||||||
"@pdf-lib/fontkit": "^1.1.1",
|
|
||||||
"@reduxjs/toolkit": "2.2.1",
|
"@reduxjs/toolkit": "2.2.1",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.4",
|
||||||
"crypto-hash": "3.0.0",
|
"crypto-hash": "3.0.0",
|
||||||
@ -1750,15 +1749,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@pdf-lib/fontkit": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-KjMd7grNapIWS/Dm0gvfHEilSyAmeLvrEGVcqLGi0VYebuqqzTbgF29efCx7tvx+IEbG3zQciRSWl3GkUSvjZg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pako": "^1.0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@pdf-lib/standard-fonts": {
|
"node_modules/@pdf-lib/standard-fonts": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
||||||
|
@ -31,7 +31,6 @@
|
|||||||
"@mui/material": "5.15.11",
|
"@mui/material": "5.15.11",
|
||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@nostr-dev-kit/ndk": "2.5.0",
|
"@nostr-dev-kit/ndk": "2.5.0",
|
||||||
"@pdf-lib/fontkit": "^1.1.1",
|
|
||||||
"@reduxjs/toolkit": "2.2.1",
|
"@reduxjs/toolkit": "2.2.1",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.4",
|
||||||
"crypto-hash": "3.0.0",
|
"crypto-hash": "3.0.0",
|
||||||
|
14
src/App.scss
14
src/App.scss
@ -135,18 +135,12 @@ li {
|
|||||||
// Consistent styling for every file mark
|
// Consistent styling for every file mark
|
||||||
// Reverts some of the design defaults for font
|
// Reverts some of the design defaults for font
|
||||||
.file-mark {
|
.file-mark {
|
||||||
font-family: 'Roboto';
|
font-family: Arial;
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
letter-spacing: normal;
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
font-weight: normal;
|
||||||
color: black;
|
color: black;
|
||||||
outline: 1px solid transparent;
|
letter-spacing: normal;
|
||||||
|
border: 1px solid transparent;
|
||||||
justify-content: start;
|
|
||||||
align-items: start;
|
|
||||||
|
|
||||||
scroll-margin-top: $header-height + $body-vertical-padding;
|
scroll-margin-top: $header-height + $body-vertical-padding;
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
@ -37,7 +37,7 @@ import { setUserRobotImage } from '../../store/userRobotImage/action'
|
|||||||
import { Container } from '../Container'
|
import { Container } from '../Container'
|
||||||
import { ButtonIcon } from '../ButtonIcon'
|
import { ButtonIcon } from '../ButtonIcon'
|
||||||
|
|
||||||
const metadataController = MetadataController.getInstance()
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
export const AppBar = () => {
|
export const AppBar = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Meta } from '../../types'
|
import { Meta } from '../../types'
|
||||||
import { SigitCardDisplayInfo, SigitStatus, SignStatus } from '../../utils'
|
import { SigitCardDisplayInfo, SigitStatus, SignStatus } from '../../utils'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { formatTimestamp, npubToHex } from '../../utils'
|
import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils'
|
||||||
import { appPublicRoutes, appPrivateRoutes } from '../../routes'
|
import { appPublicRoutes, appPrivateRoutes } from '../../routes'
|
||||||
import { Button, Divider, Tooltip } from '@mui/material'
|
import { Button, Divider, Tooltip } from '@mui/material'
|
||||||
import { DisplaySigner } from '../DisplaySigner'
|
import { DisplaySigner } from '../DisplaySigner'
|
||||||
@ -17,7 +17,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { UserAvatarGroup } from '../UserAvatarGroup'
|
import { UserAvatarGroup } from '../UserAvatarGroup'
|
||||||
|
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
import { TooltipChild } from '../TooltipChild'
|
||||||
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
||||||
|
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
|
||||||
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
||||||
import { extractFileExtensions } from '../../utils/file'
|
import { extractFileExtensions } from '../../utils/file'
|
||||||
|
|
||||||
@ -31,6 +33,12 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
|
|||||||
parsedMeta
|
parsedMeta
|
||||||
|
|
||||||
const { signersStatus, fileHashes } = useSigitMeta(meta)
|
const { signersStatus, fileHashes } = useSigitMeta(meta)
|
||||||
|
|
||||||
|
const profiles = useSigitProfiles([
|
||||||
|
...(submittedBy ? [submittedBy] : []),
|
||||||
|
...signers
|
||||||
|
])
|
||||||
|
|
||||||
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
|
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -46,29 +54,62 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
|
|||||||
></Link>
|
></Link>
|
||||||
<p className={`line-clamp-2 ${styles.title}`}>{title}</p>
|
<p className={`line-clamp-2 ${styles.title}`}>{title}</p>
|
||||||
<div className={styles.users}>
|
<div className={styles.users}>
|
||||||
{submittedBy && (
|
{submittedBy &&
|
||||||
<DisplaySigner
|
(function () {
|
||||||
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
|
const profile = profiles[submittedBy]
|
||||||
pubkey={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 && signers.length ? (
|
{submittedBy && signers.length ? (
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
) : null}
|
) : null}
|
||||||
<UserAvatarGroup max={7}>
|
<UserAvatarGroup max={7}>
|
||||||
{signers.map((signer) => {
|
{signers.map((signer) => {
|
||||||
const pubkey = npubToHex(signer)!
|
const pubkey = npubToHex(signer)!
|
||||||
|
const profile = profiles[pubkey]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DisplaySigner
|
<Tooltip
|
||||||
key={pubkey}
|
key={signer}
|
||||||
status={signersStatus[signer]}
|
title={
|
||||||
pubkey={pubkey}
|
profile?.display_name || profile?.name || shorten(pubkey)
|
||||||
/>
|
}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<TooltipChild>
|
||||||
|
<DisplaySigner
|
||||||
|
status={signersStatus[signer]}
|
||||||
|
profile={profile}
|
||||||
|
pubkey={pubkey}
|
||||||
|
/>
|
||||||
|
</TooltipChild>
|
||||||
|
</Tooltip>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</UserAvatarGroup>
|
</UserAvatarGroup>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${styles.details} ${styles.iconLabel}`}>
|
<div className={`${styles.details} ${styles.date} ${styles.iconLabel}`}>
|
||||||
<FontAwesomeIcon icon={faCalendar} />
|
<FontAwesomeIcon icon={faCalendar} />
|
||||||
{createdAt ? formatTimestamp(createdAt) : null}
|
{createdAt ? formatTimestamp(createdAt) : null}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Badge } from '@mui/material'
|
import { Badge } from '@mui/material'
|
||||||
|
import { ProfileMetadata } from '../../types'
|
||||||
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'
|
||||||
@ -14,33 +15,38 @@ import { SignStatus } from '../../utils'
|
|||||||
import { Spinner } from '../Spinner'
|
import { Spinner } from '../Spinner'
|
||||||
|
|
||||||
type DisplaySignerProps = {
|
type DisplaySignerProps = {
|
||||||
|
profile: ProfileMetadata
|
||||||
pubkey: string
|
pubkey: string
|
||||||
status: SignStatus
|
status: SignStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusIcon = (status: SignStatus) => {
|
export const DisplaySigner = ({
|
||||||
switch (status) {
|
status,
|
||||||
case SignStatus.Signed:
|
profile,
|
||||||
return <FontAwesomeIcon icon={faCheck} />
|
pubkey
|
||||||
case SignStatus.Awaiting:
|
}: DisplaySignerProps) => {
|
||||||
return (
|
const getStatusIcon = (status: SignStatus) => {
|
||||||
<Spinner>
|
switch (status) {
|
||||||
<FontAwesomeIcon icon={faHourglass} />
|
case SignStatus.Signed:
|
||||||
</Spinner>
|
return <FontAwesomeIcon icon={faCheck} />
|
||||||
)
|
case SignStatus.Awaiting:
|
||||||
case SignStatus.Pending:
|
return (
|
||||||
return <FontAwesomeIcon icon={faEllipsis} />
|
<Spinner>
|
||||||
case SignStatus.Invalid:
|
<FontAwesomeIcon icon={faHourglass} />
|
||||||
return <FontAwesomeIcon icon={faExclamation} />
|
</Spinner>
|
||||||
case SignStatus.Viewer:
|
)
|
||||||
return <FontAwesomeIcon icon={faEye} />
|
case SignStatus.Pending:
|
||||||
|
return <FontAwesomeIcon icon={faEllipsis} />
|
||||||
|
case SignStatus.Invalid:
|
||||||
|
return <FontAwesomeIcon icon={faExclamation} />
|
||||||
|
case SignStatus.Viewer:
|
||||||
|
return <FontAwesomeIcon icon={faEye} />
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <FontAwesomeIcon icon={faQuestion} />
|
return <FontAwesomeIcon icon={faQuestion} />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export const DisplaySigner = ({ status, pubkey }: DisplaySignerProps) => {
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
className={styles.signer}
|
className={styles.signer}
|
||||||
@ -50,7 +56,7 @@ export const DisplaySigner = ({ status, pubkey }: DisplaySignerProps) => {
|
|||||||
<div className={styles.statusBadge}>{getStatusIcon(status)}</div>
|
<div className={styles.statusBadge}>{getStatusIcon(status)}</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<UserAvatar pubkey={pubkey} />
|
<UserAvatar pubkey={pubkey} image={profile?.picture} />
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,9 @@ import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
|
|||||||
import { truncate } from 'lodash'
|
import { truncate } from 'lodash'
|
||||||
import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils'
|
import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils'
|
||||||
import { getSigitFile, SigitFile } from '../../utils/file'
|
import { getSigitFile, SigitFile } from '../../utils/file'
|
||||||
import { getToolboxLabelByMarkType } from '../../utils/mark'
|
|
||||||
import { FileDivider } from '../FileDivider'
|
import { FileDivider } from '../FileDivider'
|
||||||
import { ExtensionFileBox } from '../ExtensionFileBox'
|
import { ExtensionFileBox } from '../ExtensionFileBox'
|
||||||
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf'
|
import { inPx } from '../../utils/pdf'
|
||||||
import { useScale } from '../../hooks/useScale'
|
import { useScale } from '../../hooks/useScale'
|
||||||
import { AvatarIconButton } from '../UserAvatarIconButton'
|
import { AvatarIconButton } from '../UserAvatarIconButton'
|
||||||
import { LoadingSpinner } from '../LoadingSpinner'
|
import { LoadingSpinner } from '../LoadingSpinner'
|
||||||
@ -391,7 +390,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
backgroundColor: drawnField.counterpart
|
backgroundColor: drawnField.counterpart
|
||||||
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}4b`
|
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}4b`
|
||||||
: undefined,
|
: undefined,
|
||||||
outlineColor: drawnField.counterpart
|
borderColor: drawnField.counterpart
|
||||||
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}`
|
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}`
|
||||||
: undefined,
|
: undefined,
|
||||||
left: inPx(from(page.width, drawnField.left)),
|
left: inPx(from(page.width, drawnField.left)),
|
||||||
@ -407,16 +406,6 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
: undefined
|
: undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className={`file-mark ${styles.placeholder}`}
|
|
||||||
style={{
|
|
||||||
fontFamily: FONT_TYPE,
|
|
||||||
fontSize: inPx(from(page.width, FONT_SIZE))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getToolboxLabelByMarkType(drawnField.type) ||
|
|
||||||
'placeholder'}
|
|
||||||
</div>
|
|
||||||
<span
|
<span
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
handleResizePointerDown(event, drawnFieldIndex)
|
handleResizePointerDown(event, drawnFieldIndex)
|
||||||
|
@ -13,15 +13,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0.5;
|
|
||||||
inset: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawingRectangle {
|
.drawingRectangle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
outline: 1px solid #01aaad;
|
border: 1px solid #01aaad;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
background-color: #01aaad4b;
|
background-color: #01aaad4b;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -35,7 +29,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.edited {
|
&.edited {
|
||||||
outline: 1px dotted #01aaad;
|
border: 1px dotted #01aaad;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resizeHandle {
|
.resizeHandle {
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { createPortal } from 'react-dom'
|
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { PropsWithChildren } from 'react'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
desc?: string
|
desc?: string
|
||||||
variant?: 'small' | 'default'
|
variant?: 'small' | 'default'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoadingSpinner = (props: PropsWithChildren<Props>) => {
|
export const LoadingSpinner = (props: Props) => {
|
||||||
const { desc, children, variant = 'default' } = props
|
const { desc, variant = 'default' } = props
|
||||||
|
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'small':
|
case 'small':
|
||||||
@ -22,22 +20,16 @@ export const LoadingSpinner = (props: PropsWithChildren<Props>) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return createPortal(
|
return (
|
||||||
<div className={styles.loadingSpinnerOverlay}>
|
<div className={styles.loadingSpinnerOverlay}>
|
||||||
<div
|
<div
|
||||||
className={styles.loadingSpinnerContainer}
|
className={styles.loadingSpinnerContainer}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
>
|
>
|
||||||
<div className={styles.loadingSpinner}></div>
|
<div className={styles.loadingSpinner}></div>
|
||||||
{desc && (
|
{desc && <p className={styles.loadingSpinnerDesc}>{desc}</p>}
|
||||||
<div className={styles.loadingSpinnerDesc}>
|
|
||||||
{desc}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>
|
||||||
document.getElementById('root')!
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,15 +42,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-top: solid 1px rgba(0, 0, 0, 0.1);
|
border-top: solid 1px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
color: rgba(0, 0, 0, 0.5);
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
|
@ -36,7 +36,7 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
|
|||||||
backgroundColor: selectedMark?.mark.npub
|
backgroundColor: selectedMark?.mark.npub
|
||||||
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}4b`
|
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}4b`
|
||||||
: undefined,
|
: undefined,
|
||||||
outlineColor: selectedMark?.mark.npub
|
borderColor: selectedMark?.mark.npub
|
||||||
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}`
|
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}`
|
||||||
: undefined,
|
: undefined,
|
||||||
left: inPx(from(pageWidth, location.left)),
|
left: inPx(from(pageWidth, location.left)),
|
||||||
|
@ -3,56 +3,34 @@ import { getProfileRoute } from '../../routes'
|
|||||||
import styles from './styles.module.scss'
|
import styles from './styles.module.scss'
|
||||||
import { AvatarIconButton } from '../UserAvatarIconButton'
|
import { AvatarIconButton } from '../UserAvatarIconButton'
|
||||||
import { Link } from 'react-router-dom'
|
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 {
|
interface UserAvatarProps {
|
||||||
|
name?: string
|
||||||
pubkey: string
|
pubkey: string
|
||||||
isNameVisible?: boolean
|
image?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component will be used for the displaying username and profile picture.
|
* This component will be used for the displaying username and profile picture.
|
||||||
* Clicking will navigate to the user's profile.
|
* Clicking will navigate to the user's profile.
|
||||||
*/
|
*/
|
||||||
export const UserAvatar = ({
|
export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => {
|
||||||
pubkey,
|
|
||||||
isNameVisible = false
|
|
||||||
}: UserAvatarProps) => {
|
|
||||||
const profile = useProfileMetadata(pubkey)
|
|
||||||
const name = profile?.display_name || profile?.name || shorten(pubkey)
|
|
||||||
const image = profile?.picture
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={getProfileRoute(pubkey)}
|
to={getProfileRoute(pubkey)}
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<AvatarIconButton
|
||||||
key={pubkey}
|
src={image}
|
||||||
title={name}
|
hexKey={pubkey}
|
||||||
placement="top"
|
aria-label={`account of user ${name || pubkey}`}
|
||||||
arrow
|
color="inherit"
|
||||||
disableInteractive
|
sx={{
|
||||||
>
|
padding: 0
|
||||||
<TooltipChild>
|
}}
|
||||||
<AvatarIconButton
|
/>
|
||||||
src={image}
|
{name ? <span className={styles.username}>{name}</span> : null}
|
||||||
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>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { Divider, Tooltip } from '@mui/material'
|
import { Divider, Tooltip } from '@mui/material'
|
||||||
|
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
|
||||||
import {
|
import {
|
||||||
formatTimestamp,
|
formatTimestamp,
|
||||||
fromUnixTimestamp,
|
fromUnixTimestamp,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
|
shorten,
|
||||||
SignStatus
|
SignStatus
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
||||||
@ -22,10 +24,10 @@ import {
|
|||||||
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { State } from '../../store/rootReducer'
|
import { State } from '../../store/rootReducer'
|
||||||
|
import { TooltipChild } from '../TooltipChild'
|
||||||
import { DisplaySigner } from '../DisplaySigner'
|
import { DisplaySigner } from '../DisplaySigner'
|
||||||
import { Meta } from '../../types'
|
import { Meta } from '../../types'
|
||||||
import { extractFileExtensions } from '../../utils/file'
|
import { extractFileExtensions } from '../../utils/file'
|
||||||
import { UserAvatar } from '../UserAvatar'
|
|
||||||
|
|
||||||
interface UsersDetailsProps {
|
interface UsersDetailsProps {
|
||||||
meta: Meta
|
meta: Meta
|
||||||
@ -34,7 +36,6 @@ interface UsersDetailsProps {
|
|||||||
export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||||
const {
|
const {
|
||||||
submittedBy,
|
submittedBy,
|
||||||
exportedBy,
|
|
||||||
signers,
|
signers,
|
||||||
viewers,
|
viewers,
|
||||||
fileHashes,
|
fileHashes,
|
||||||
@ -46,6 +47,11 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
isValid
|
isValid
|
||||||
} = useSigitMeta(meta)
|
} = useSigitMeta(meta)
|
||||||
const { usersPubkey } = useSelector((state: State) => state.auth)
|
const { usersPubkey } = useSelector((state: State) => state.auth)
|
||||||
|
const profiles = useSigitProfiles([
|
||||||
|
...(submittedBy ? [submittedBy] : []),
|
||||||
|
...signers,
|
||||||
|
...viewers
|
||||||
|
])
|
||||||
const userCanSign =
|
const userCanSign =
|
||||||
typeof usersPubkey !== 'undefined' &&
|
typeof usersPubkey !== 'undefined' &&
|
||||||
signers.includes(hexToNpub(usersPubkey))
|
signers.includes(hexToNpub(usersPubkey))
|
||||||
@ -57,12 +63,31 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<p>Signers</p>
|
<p>Signers</p>
|
||||||
<div className={styles.users}>
|
<div className={styles.users}>
|
||||||
{submittedBy && (
|
{submittedBy &&
|
||||||
<DisplaySigner
|
(function () {
|
||||||
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
|
const profile = profiles[submittedBy]
|
||||||
pubkey={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 && signers.length ? (
|
{submittedBy && signers.length ? (
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
@ -71,12 +96,26 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
<UserAvatarGroup max={20}>
|
<UserAvatarGroup max={20}>
|
||||||
{signers.map((signer) => {
|
{signers.map((signer) => {
|
||||||
const pubkey = npubToHex(signer)!
|
const pubkey = npubToHex(signer)!
|
||||||
|
const profile = profiles[pubkey]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DisplaySigner
|
<Tooltip
|
||||||
key={pubkey}
|
key={signer}
|
||||||
status={signersStatus[signer]}
|
title={
|
||||||
pubkey={pubkey}
|
profile?.display_name || profile?.name || shorten(pubkey)
|
||||||
/>
|
}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<TooltipChild>
|
||||||
|
<DisplaySigner
|
||||||
|
status={signersStatus[signer]}
|
||||||
|
profile={profile}
|
||||||
|
pubkey={pubkey}
|
||||||
|
/>
|
||||||
|
</TooltipChild>
|
||||||
|
</Tooltip>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</UserAvatarGroup>
|
</UserAvatarGroup>
|
||||||
@ -89,28 +128,34 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
<UserAvatarGroup max={20}>
|
<UserAvatarGroup max={20}>
|
||||||
{viewers.map((signer) => {
|
{viewers.map((signer) => {
|
||||||
const pubkey = npubToHex(signer)!
|
const pubkey = npubToHex(signer)!
|
||||||
|
const profile = profiles[pubkey]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DisplaySigner
|
<Tooltip
|
||||||
key={pubkey}
|
key={signer}
|
||||||
status={SignStatus.Viewer}
|
title={
|
||||||
pubkey={pubkey}
|
profile?.display_name ||
|
||||||
/>
|
profile?.name ||
|
||||||
|
shorten(pubkey)
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<TooltipChild>
|
||||||
|
<DisplaySigner
|
||||||
|
status={SignStatus.Viewer}
|
||||||
|
profile={profile}
|
||||||
|
pubkey={pubkey}
|
||||||
|
/>
|
||||||
|
</TooltipChild>
|
||||||
|
</Tooltip>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</UserAvatarGroup>
|
</UserAvatarGroup>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{exportedBy && (
|
|
||||||
<>
|
|
||||||
<p>Exported By</p>
|
|
||||||
<div className={styles.users}>
|
|
||||||
<UserAvatar pubkey={exportedBy} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<p>Details</p>
|
<p>Details</p>
|
||||||
|
@ -25,7 +25,7 @@ export class AuthController {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.nostrController = NostrController.getInstance()
|
this.nostrController = NostrController.getInstance()
|
||||||
this.metadataController = MetadataController.getInstance()
|
this.metadataController = new MetadataController()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,7 +22,6 @@ import {
|
|||||||
import { DEFAULT_LOOK_UP_RELAY_LIST } from '../utils/const'
|
import { DEFAULT_LOOK_UP_RELAY_LIST } from '../utils/const'
|
||||||
|
|
||||||
export class MetadataController extends EventEmitter {
|
export class MetadataController extends EventEmitter {
|
||||||
private static instance: MetadataController
|
|
||||||
private nostrController: NostrController
|
private nostrController: NostrController
|
||||||
private specialMetadataRelay = 'wss://purplepag.es'
|
private specialMetadataRelay = 'wss://purplepag.es'
|
||||||
private pendingFetches = new Map<string, Promise<Event | null>>() // Track pending fetches
|
private pendingFetches = new Map<string, Promise<Event | null>>() // Track pending fetches
|
||||||
@ -32,13 +31,6 @@ export class MetadataController extends EventEmitter {
|
|||||||
this.nostrController = NostrController.getInstance()
|
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.
|
* 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.
|
* If a more recent metadata event is found, it is handled and returned.
|
||||||
@ -127,6 +119,7 @@ export class MetadataController extends EventEmitter {
|
|||||||
// Check if the cached metadata is older than one day
|
// Check if the cached metadata is older than one day
|
||||||
if (isOlderThanOneDay(cachedMetadataEvent.cachedAt)) {
|
if (isOlderThanOneDay(cachedMetadataEvent.cachedAt)) {
|
||||||
// If older than one week, find the metadata from relays in background
|
// If older than one week, find the metadata from relays in background
|
||||||
|
|
||||||
this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event)
|
this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
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,10 +33,6 @@ export interface FlatMeta
|
|||||||
// Remove pubkey and use submittedBy as `npub1${string}`
|
// Remove pubkey and use submittedBy as `npub1${string}`
|
||||||
submittedBy?: `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
|
// Remove created_at and replace with createdAt
|
||||||
createdAt?: number
|
createdAt?: number
|
||||||
|
|
||||||
@ -72,7 +68,6 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
const [tags, setTags] = useState<string[][]>()
|
const [tags, setTags] = useState<string[][]>()
|
||||||
const [createdAt, setCreatedAt] = useState<number>()
|
const [createdAt, setCreatedAt] = useState<number>()
|
||||||
const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event
|
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 [id, setId] = useState<string>()
|
||||||
const [sig, setSig] = useState<string>()
|
const [sig, setSig] = useState<string>()
|
||||||
|
|
||||||
@ -104,18 +99,6 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
if (!meta) return
|
if (!meta) return
|
||||||
;(async function () {
|
;(async function () {
|
||||||
try {
|
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 createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
||||||
|
|
||||||
const { kind, tags, created_at, pubkey, id, sig, content } =
|
const { kind, tags, created_at, pubkey, id, sig, content } =
|
||||||
@ -282,7 +265,6 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
tags,
|
tags,
|
||||||
createdAt,
|
createdAt,
|
||||||
submittedBy,
|
submittedBy,
|
||||||
exportedBy,
|
|
||||||
id,
|
id,
|
||||||
sig,
|
sig,
|
||||||
signers,
|
signers,
|
||||||
|
71
src/hooks/useSigitProfiles.tsx
Normal file
71
src/hooks/useSigitProfiles.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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)
|
const hasSubscribed = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const metadataController = MetadataController.getInstance()
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
dispatch(
|
dispatch(
|
||||||
|
@ -36,26 +36,44 @@ import {
|
|||||||
npubToHex,
|
npubToHex,
|
||||||
queryNip05,
|
queryNip05,
|
||||||
sendNotification,
|
sendNotification,
|
||||||
|
shorten,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
updateUsersAppData,
|
updateUsersAppData,
|
||||||
uploadToFileStorage,
|
uploadToFileStorage
|
||||||
DEFAULT_TOOLBOX
|
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import fileListStyles from '../../components/FileList/style.module.scss'
|
import fileListStyles from '../../components/FileList/style.module.scss'
|
||||||
import { DrawTool } from '../../types/drawing'
|
import { DrawTool, MarkType } from '../../types/drawing'
|
||||||
import { DrawPDFFields } from '../../components/DrawPDFFields'
|
import { DrawPDFFields } from '../../components/DrawPDFFields'
|
||||||
import { Mark } from '../../types/mark.ts'
|
import { Mark } from '../../types/mark.ts'
|
||||||
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import {
|
import {
|
||||||
|
fa1,
|
||||||
|
faBriefcase,
|
||||||
|
faCalendarDays,
|
||||||
|
faCheckDouble,
|
||||||
|
faCircleDot,
|
||||||
|
faClock,
|
||||||
|
faCreditCard,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEye,
|
faEye,
|
||||||
faFile,
|
faFile,
|
||||||
faFileCirclePlus,
|
faFileCirclePlus,
|
||||||
faGripLines,
|
faGripLines,
|
||||||
|
faHeading,
|
||||||
|
faIdCard,
|
||||||
|
faImage,
|
||||||
|
faPaperclip,
|
||||||
faPen,
|
faPen,
|
||||||
|
faPhone,
|
||||||
faPlus,
|
faPlus,
|
||||||
|
faSignature,
|
||||||
|
faSquareCaretDown,
|
||||||
|
faSquareCheck,
|
||||||
|
faStamp,
|
||||||
|
faT,
|
||||||
|
faTableCellsLarge,
|
||||||
faToolbox,
|
faToolbox,
|
||||||
faTrash,
|
faTrash,
|
||||||
faUpload
|
faUpload
|
||||||
@ -95,8 +113,6 @@ export const CreatePage = () => {
|
|||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
|
|
||||||
const [users, setUsers] = useState<User[]>([])
|
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)
|
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||||
|
|
||||||
@ -108,7 +124,116 @@ export const CreatePage = () => {
|
|||||||
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
|
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
|
||||||
|
|
||||||
const [selectedTool, setSelectedTool] = useState<DrawTool>()
|
const [selectedTool, setSelectedTool] = useState<DrawTool>()
|
||||||
const [toolbox] = useState<DrawTool[]>(DEFAULT_TOOLBOX)
|
const [toolbox] = useState<DrawTool[]>([
|
||||||
|
{
|
||||||
|
identifier: MarkType.TEXT,
|
||||||
|
icon: faT,
|
||||||
|
label: 'Text',
|
||||||
|
active: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.SIGNATURE,
|
||||||
|
icon: faSignature,
|
||||||
|
label: 'Signature',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.JOBTITLE,
|
||||||
|
icon: faBriefcase,
|
||||||
|
label: 'Job Title',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.FULLNAME,
|
||||||
|
icon: faIdCard,
|
||||||
|
label: 'Full Name',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.INITIALS,
|
||||||
|
icon: faHeading,
|
||||||
|
label: 'Initials',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.DATETIME,
|
||||||
|
icon: faClock,
|
||||||
|
label: 'Date Time',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.DATE,
|
||||||
|
icon: faCalendarDays,
|
||||||
|
label: 'Date',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.NUMBER,
|
||||||
|
icon: fa1,
|
||||||
|
label: 'Number',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.IMAGES,
|
||||||
|
icon: faImage,
|
||||||
|
label: 'Images',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.CHECKBOX,
|
||||||
|
icon: faSquareCheck,
|
||||||
|
label: 'Checkbox',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.MULTIPLE,
|
||||||
|
icon: faCheckDouble,
|
||||||
|
label: 'Multiple',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.FILE,
|
||||||
|
icon: faPaperclip,
|
||||||
|
label: 'File',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.RADIO,
|
||||||
|
icon: faCircleDot,
|
||||||
|
label: 'Radio',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.SELECT,
|
||||||
|
icon: faSquareCaretDown,
|
||||||
|
label: 'Select',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.CELLS,
|
||||||
|
icon: faTableCellsLarge,
|
||||||
|
label: 'Cells',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.STAMP,
|
||||||
|
icon: faStamp,
|
||||||
|
label: 'Stamp',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.PAYMENT,
|
||||||
|
icon: faCreditCard,
|
||||||
|
label: 'Payment',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.PHONE,
|
||||||
|
icon: faPhone,
|
||||||
|
label: 'Phone',
|
||||||
|
active: false
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the drawing tool
|
* Changes the drawing tool
|
||||||
@ -127,7 +252,7 @@ export const CreatePage = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
users.forEach((user) => {
|
users.forEach((user) => {
|
||||||
if (!(user.pubkey in metadata)) {
|
if (!(user.pubkey in metadata)) {
|
||||||
const metadataController = MetadataController.getInstance()
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
const handleMetadataEvent = (event: Event) => {
|
const handleMetadataEvent = (event: Event) => {
|
||||||
const metadataContent =
|
const metadataContent =
|
||||||
@ -522,11 +647,6 @@ export const CreatePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveAs(finalZipFile, `request-${unixNow()}.sigit.zip`)
|
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)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,6 +672,9 @@ export const CreatePage = () => {
|
|||||||
},
|
},
|
||||||
zipUrl: string
|
zipUrl: string
|
||||||
) => {
|
) => {
|
||||||
|
const signers = users.filter((user) => user.role === UserRole.signer)
|
||||||
|
const viewers = users.filter((user) => user.role === UserRole.viewer)
|
||||||
|
|
||||||
const content: CreateSignatureEventContent = {
|
const content: CreateSignatureEventContent = {
|
||||||
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
|
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
|
||||||
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
|
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
|
||||||
@ -580,6 +703,9 @@ export const CreatePage = () => {
|
|||||||
|
|
||||||
// Send notifications to signers and viewers
|
// Send notifications to signers and viewers
|
||||||
const sendNotifications = (meta: Meta) => {
|
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
|
// no need to send notification to self so remove it from the list
|
||||||
const receivers = (
|
const receivers = (
|
||||||
signers.length > 0
|
signers.length > 0
|
||||||
@ -661,7 +787,7 @@ export const CreatePage = () => {
|
|||||||
toast.error('Failed to publish notifications')
|
toast.error('Failed to publish notifications')
|
||||||
})
|
})
|
||||||
|
|
||||||
navigate(appPrivateRoutes.sign, { state: { meta } })
|
navigate(appPrivateRoutes.sign, { state: { meta: meta } })
|
||||||
} else {
|
} else {
|
||||||
const zip = new JSZip()
|
const zip = new JSZip()
|
||||||
|
|
||||||
@ -788,6 +914,7 @@ export const CreatePage = () => {
|
|||||||
<div className={styles.flexWrap}>
|
<div className={styles.flexWrap}>
|
||||||
<div className={`${styles.paperGroup} ${styles.users}`}>
|
<div className={`${styles.paperGroup} ${styles.users}`}>
|
||||||
<DisplayUser
|
<DisplayUser
|
||||||
|
metadata={metadata}
|
||||||
users={users}
|
users={users}
|
||||||
handleUserRoleChange={handleUserRoleChange}
|
handleUserRoleChange={handleUserRoleChange}
|
||||||
handleRemoveUser={handleRemoveUser}
|
handleRemoveUser={handleRemoveUser}
|
||||||
@ -831,6 +958,9 @@ export const CreatePage = () => {
|
|||||||
<FontAwesomeIcon icon={faPlus} />
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<Button onClick={handleCreate} variant="contained">
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
|
||||||
<div className={`${styles.paperGroup} ${styles.toolbox}`}>
|
<div className={`${styles.paperGroup} ${styles.toolbox}`}>
|
||||||
{toolbox.map((drawTool: DrawTool, index: number) => {
|
{toolbox.map((drawTool: DrawTool, index: number) => {
|
||||||
@ -857,10 +987,6 @@ export const CreatePage = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleCreate} variant="contained">
|
|
||||||
Publish
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{!!error && (
|
{!!error && (
|
||||||
<FormHelperText error={!!error}>{error}</FormHelperText>
|
<FormHelperText error={!!error}>{error}</FormHelperText>
|
||||||
)}
|
)}
|
||||||
@ -884,6 +1010,7 @@ export const CreatePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DisplayUsersProps = {
|
type DisplayUsersProps = {
|
||||||
|
metadata: { [key: string]: ProfileMetadata }
|
||||||
users: User[]
|
users: User[]
|
||||||
handleUserRoleChange: (role: UserRole, pubkey: string) => void
|
handleUserRoleChange: (role: UserRole, pubkey: string) => void
|
||||||
handleRemoveUser: (pubkey: string) => void
|
handleRemoveUser: (pubkey: string) => void
|
||||||
@ -891,6 +1018,7 @@ type DisplayUsersProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DisplayUser = ({
|
const DisplayUser = ({
|
||||||
|
metadata,
|
||||||
users,
|
users,
|
||||||
handleUserRoleChange,
|
handleUserRoleChange,
|
||||||
handleRemoveUser,
|
handleRemoveUser,
|
||||||
@ -904,6 +1032,7 @@ const DisplayUser = ({
|
|||||||
.map((user, index) => (
|
.map((user, index) => (
|
||||||
<SignerCounterpart
|
<SignerCounterpart
|
||||||
key={`signer-${user.pubkey}`}
|
key={`signer-${user.pubkey}`}
|
||||||
|
userMeta={metadata[user.pubkey]}
|
||||||
user={user}
|
user={user}
|
||||||
index={index}
|
index={index}
|
||||||
moveSigner={moveSigner}
|
moveSigner={moveSigner}
|
||||||
@ -918,6 +1047,7 @@ const DisplayUser = ({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.user} key={`viewer-${user.pubkey}`}>
|
<div className={styles.user} key={`viewer-${user.pubkey}`}>
|
||||||
<Counterpart
|
<Counterpart
|
||||||
|
userMeta={metadata[user.pubkey]}
|
||||||
user={user}
|
user={user}
|
||||||
handleUserRoleChange={handleUserRoleChange}
|
handleUserRoleChange={handleUserRoleChange}
|
||||||
handleRemoveUser={handleRemoveUser}
|
handleRemoveUser={handleRemoveUser}
|
||||||
@ -936,6 +1066,7 @@ interface DragItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CounterpartProps = {
|
type CounterpartProps = {
|
||||||
|
userMeta: ProfileMetadata
|
||||||
user: User
|
user: User
|
||||||
handleUserRoleChange: (role: UserRole, pubkey: string) => void
|
handleUserRoleChange: (role: UserRole, pubkey: string) => void
|
||||||
handleRemoveUser: (pubkey: string) => void
|
handleRemoveUser: (pubkey: string) => void
|
||||||
@ -947,6 +1078,7 @@ type SignerCounterpartProps = CounterpartProps & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SignerCounterpart = ({
|
const SignerCounterpart = ({
|
||||||
|
userMeta,
|
||||||
user,
|
user,
|
||||||
index,
|
index,
|
||||||
moveSigner,
|
moveSigner,
|
||||||
@ -1039,6 +1171,7 @@ const SignerCounterpart = ({
|
|||||||
<FontAwesomeIcon width={'14px'} fontSize={'14px'} icon={faGripLines} />
|
<FontAwesomeIcon width={'14px'} fontSize={'14px'} icon={faGripLines} />
|
||||||
<Counterpart
|
<Counterpart
|
||||||
user={user}
|
user={user}
|
||||||
|
userMeta={userMeta}
|
||||||
handleRemoveUser={handleRemoveUser}
|
handleRemoveUser={handleRemoveUser}
|
||||||
handleUserRoleChange={handleUserRoleChange}
|
handleUserRoleChange={handleUserRoleChange}
|
||||||
/>
|
/>
|
||||||
@ -1047,6 +1180,7 @@ const SignerCounterpart = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Counterpart = ({
|
const Counterpart = ({
|
||||||
|
userMeta,
|
||||||
user,
|
user,
|
||||||
handleUserRoleChange,
|
handleUserRoleChange,
|
||||||
handleRemoveUser
|
handleRemoveUser
|
||||||
@ -1054,7 +1188,15 @@ const Counterpart = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.avatar}>
|
<div className={styles.avatar}>
|
||||||
<UserAvatar pubkey={user.pubkey} isNameVisible={true} />
|
<UserAvatar
|
||||||
|
pubkey={user.pubkey}
|
||||||
|
name={
|
||||||
|
userMeta?.display_name ||
|
||||||
|
userMeta?.name ||
|
||||||
|
shorten(hexToNpub(user.pubkey))
|
||||||
|
}
|
||||||
|
image={userMeta?.picture}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip title="Toggle User Role" arrow disableInteractive>
|
<Tooltip title="Toggle User Role" arrow disableInteractive>
|
||||||
<Button
|
<Button
|
||||||
|
@ -18,16 +18,12 @@ import {
|
|||||||
} from '../../store/actions'
|
} from '../../store/actions'
|
||||||
import { LoginMethods } from '../../store/auth/types'
|
import { LoginMethods } from '../../store/auth/types'
|
||||||
import { Dispatch } from '../../store/store'
|
import { Dispatch } from '../../store/store'
|
||||||
import { npubToHex, queryNip05, timeout } from '../../utils'
|
import { npubToHex, queryNip05 } from '../../utils'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
import { NIP05_REGEX } from '../../constants'
|
import { NIP05_REGEX } from '../../constants'
|
||||||
|
|
||||||
import styles from './styles.module.scss'
|
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 = () => {
|
export const Nostr = () => {
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
@ -35,12 +31,11 @@ export const Nostr = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const authController = new AuthController()
|
const authController = new AuthController()
|
||||||
const metadataController = MetadataController.getInstance()
|
const metadataController = new MetadataController()
|
||||||
const nostrController = NostrController.getInstance()
|
const nostrController = NostrController.getInstance()
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
const [isExtensionSlow, setIsExtensionSlow] = useState(false)
|
|
||||||
const [inputValue, setInputValue] = useState('')
|
const [inputValue, setInputValue] = useState('')
|
||||||
const [authUrl, setAuthUrl] = useState<string>()
|
const [authUrl, setAuthUrl] = useState<string>()
|
||||||
|
|
||||||
@ -77,43 +72,27 @@ export const Nostr = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loginWithExtension = async () => {
|
const loginWithExtension = async () => {
|
||||||
let waitTimeout: number | undefined
|
setIsLoading(true)
|
||||||
try {
|
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
||||||
// Wait EXTENSION_LOGIN_DELAY_SECONDS before showing extension delay message
|
|
||||||
waitTimeout = window.setTimeout(() => {
|
|
||||||
setIsExtensionSlow(true)
|
|
||||||
}, EXTENSION_LOGIN_DELAY_SECONDS * 1000)
|
|
||||||
|
|
||||||
setIsLoading(true)
|
nostrController
|
||||||
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
.capturePublicKey()
|
||||||
|
.then(async (pubkey) => {
|
||||||
|
dispatch(updateLoginMethod(LoginMethods.extension))
|
||||||
|
|
||||||
const pubkey = await nostrController.capturePublicKey()
|
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||||
dispatch(updateLoginMethod(LoginMethods.extension))
|
const redirectPath =
|
||||||
|
await authController.authAndGetMetadataAndRelaysMap(pubkey)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
if (redirectPath) navigateAfterLogin(redirectPath)
|
||||||
const redirectPath = await Promise.race([
|
})
|
||||||
authController.authAndGetMetadataAndRelaysMap(pubkey),
|
.catch((err) => {
|
||||||
timeout(EXTENSION_LOGIN_TIMEOUT_SECONDS * 1000)
|
toast.error('Error capturing public key from nostr extension: ' + err)
|
||||||
])
|
})
|
||||||
|
.finally(() => {
|
||||||
if (redirectPath) {
|
setIsLoading(false)
|
||||||
navigateAfterLogin(redirectPath)
|
setLoadingSpinnerDesc('')
|
||||||
}
|
})
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -375,33 +354,7 @@ export const Nostr = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && (
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
<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 && (
|
{isNostrExtensionAvailable && (
|
||||||
<>
|
<>
|
||||||
|
@ -27,7 +27,7 @@ export const ProfilePage = () => {
|
|||||||
|
|
||||||
const { npub } = useParams()
|
const { npub } = useParams()
|
||||||
|
|
||||||
const metadataController = useMemo(() => MetadataController.getInstance(), [])
|
const metadataController = useMemo(() => new MetadataController(), [])
|
||||||
|
|
||||||
const [pubkey, setPubkey] = useState<string>()
|
const [pubkey, setPubkey] = useState<string>()
|
||||||
const [nostrJoiningBlock, setNostrJoiningBlock] =
|
const [nostrJoiningBlock, setNostrJoiningBlock] =
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
useTheme
|
useTheme
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { UnsignedEvent, nip19, kinds, VerifiedEvent, Event } from 'nostr-tools'
|
import { UnsignedEvent, nip19, kinds, VerifiedEvent, Event } from 'nostr-tools'
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { MetadataController, NostrController } from '../../../controllers'
|
import { MetadataController, NostrController } from '../../../controllers'
|
||||||
@ -41,7 +41,7 @@ export const ProfileSettingsPage = () => {
|
|||||||
|
|
||||||
const dispatch: Dispatch = useDispatch()
|
const dispatch: Dispatch = useDispatch()
|
||||||
|
|
||||||
const metadataController = MetadataController.getInstance()
|
const metadataController = useMemo(() => new MetadataController(), [])
|
||||||
const nostrController = NostrController.getInstance()
|
const nostrController = NostrController.getInstance()
|
||||||
|
|
||||||
const [pubkey, setPubkey] = useState<string>()
|
const [pubkey, setPubkey] = useState<string>()
|
||||||
|
@ -33,8 +33,7 @@ import {
|
|||||||
sendNotification,
|
sendNotification,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
updateUsersAppData,
|
updateUsersAppData,
|
||||||
findOtherUserMarks,
|
findOtherUserMarks
|
||||||
timeout
|
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import { DisplayMeta } from './internal/displayMeta'
|
import { DisplayMeta } from './internal/displayMeta'
|
||||||
@ -277,10 +276,17 @@ export const SignPage = () => {
|
|||||||
setAuthUrl(url)
|
setAuthUrl(url)
|
||||||
})
|
})
|
||||||
|
|
||||||
// decrypt the encryptionKey, with timeout (duration = 60 seconds)
|
// 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
|
||||||
const encryptionKey = await Promise.race([
|
const encryptionKey = await Promise.race([
|
||||||
nostrController.nip04Decrypt(sender, key),
|
nostrController.nip04Decrypt(sender, key),
|
||||||
timeout(60000)
|
timeoutPromise
|
||||||
])
|
])
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
return res
|
return res
|
||||||
@ -462,20 +468,20 @@ export const SignPage = () => {
|
|||||||
const fileNames = Object.values(zip.files)
|
const fileNames = Object.values(zip.files)
|
||||||
.filter((entry) => entry.name.startsWith('files/') && !entry.dir)
|
.filter((entry) => entry.name.startsWith('files/') && !entry.dir)
|
||||||
.map((entry) => entry.name)
|
.map((entry) => entry.name)
|
||||||
|
.map((entry) => entry.replace(/^files\//, ''))
|
||||||
|
|
||||||
for (const zipFilePath of fileNames) {
|
// 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) {
|
||||||
const arrayBuffer = await readContentOfZipEntry(
|
const arrayBuffer = await readContentOfZipEntry(
|
||||||
zip,
|
zip,
|
||||||
zipFilePath,
|
fileName,
|
||||||
'arraybuffer'
|
'arraybuffer'
|
||||||
)
|
)
|
||||||
|
|
||||||
const fileName = zipFilePath.replace(/^files\//, '')
|
|
||||||
if (arrayBuffer) {
|
if (arrayBuffer) {
|
||||||
files[fileName] = await convertToSigitFile(arrayBuffer, fileName)
|
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)
|
const hash = await getHash(arrayBuffer)
|
||||||
if (hash) {
|
if (hash) {
|
||||||
fileHashes[fileName] = hash
|
fileHashes[fileName] = hash
|
||||||
|
@ -32,7 +32,7 @@ import { useState, useEffect } from 'react'
|
|||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { UserAvatar } from '../../../components/UserAvatar'
|
import { UserAvatar } from '../../../components/UserAvatar'
|
||||||
import { MetadataController } from '../../../controllers'
|
import { MetadataController } from '../../../controllers'
|
||||||
import { npubToHex, hexToNpub, parseJson } from '../../../utils'
|
import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils'
|
||||||
import styles from '../style.module.scss'
|
import styles from '../style.module.scss'
|
||||||
import { SigitFile } from '../../../utils/file'
|
import { SigitFile } from '../../../utils/file'
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ export const DisplayMeta = ({
|
|||||||
}, [signers, viewers])
|
}, [signers, viewers])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const metadataController = MetadataController.getInstance()
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
const hexKeys: string[] = [
|
const hexKeys: string[] = [
|
||||||
npubToHex(submittedBy)!,
|
npubToHex(submittedBy)!,
|
||||||
@ -167,7 +167,20 @@ export const DisplayMeta = ({
|
|||||||
<Typography variant="h6" sx={{ color: textColor }}>
|
<Typography variant="h6" sx={{ color: textColor }}>
|
||||||
Submitted By
|
Submitted By
|
||||||
</Typography>
|
</Typography>
|
||||||
<UserAvatar pubkey={submittedBy} isNameVisible={true} />
|
{(function () {
|
||||||
|
const profile = metadata[submittedBy]
|
||||||
|
return (
|
||||||
|
<UserAvatar
|
||||||
|
pubkey={submittedBy}
|
||||||
|
name={
|
||||||
|
profile?.display_name ||
|
||||||
|
profile?.name ||
|
||||||
|
shorten(hexToNpub(submittedBy))
|
||||||
|
}
|
||||||
|
image={profile?.picture}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
sx={{
|
sx={{
|
||||||
@ -267,12 +280,14 @@ type DisplayUserProps = {
|
|||||||
const DisplayUser = ({
|
const DisplayUser = ({
|
||||||
meta,
|
meta,
|
||||||
user,
|
user,
|
||||||
|
metadata,
|
||||||
signedBy,
|
signedBy,
|
||||||
nextSigner,
|
nextSigner,
|
||||||
getPrevSignersSig
|
getPrevSignersSig
|
||||||
}: DisplayUserProps) => {
|
}: DisplayUserProps) => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
|
const userMeta = metadata[user.pubkey]
|
||||||
const [userStatus, setUserStatus] = useState<UserStatus>(UserStatus.Pending)
|
const [userStatus, setUserStatus] = useState<UserStatus>(UserStatus.Pending)
|
||||||
const [prevSignatureStatus, setPreviousSignatureStatus] =
|
const [prevSignatureStatus, setPreviousSignatureStatus] =
|
||||||
useState<PrevSignatureValidationEnum>(PrevSignatureValidationEnum.Pending)
|
useState<PrevSignatureValidationEnum>(PrevSignatureValidationEnum.Pending)
|
||||||
@ -355,7 +370,15 @@ const DisplayUser = ({
|
|||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className={styles.tableCell}>
|
<TableCell className={styles.tableCell}>
|
||||||
<UserAvatar pubkey={user.pubkey} isNameVisible={true} />
|
<UserAvatar
|
||||||
|
pubkey={user.pubkey}
|
||||||
|
name={
|
||||||
|
userMeta?.display_name ||
|
||||||
|
userMeta?.name ||
|
||||||
|
shorten(hexToNpub(user.pubkey))
|
||||||
|
}
|
||||||
|
image={userMeta?.picture}
|
||||||
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={styles.tableCell}>{user.role}</TableCell>
|
<TableCell className={styles.tableCell}>{user.role}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import { Box, Button, Typography } from '@mui/material'
|
import { Box, Button, Tooltip, Typography } from '@mui/material'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { MuiFileInput } from 'mui-file-input'
|
import { MuiFileInput } from 'mui-file-input'
|
||||||
|
import { Event, verifyEvent } from 'nostr-tools'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { DocSignatureEvent, Meta } from '../../types'
|
import {
|
||||||
|
CreateSignatureEventContent,
|
||||||
|
DocSignatureEvent,
|
||||||
|
Meta
|
||||||
|
} from '../../types'
|
||||||
import {
|
import {
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
extractMarksFromSignedMeta,
|
extractMarksFromSignedMeta,
|
||||||
@ -15,6 +20,7 @@ import {
|
|||||||
parseJson,
|
parseJson,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
|
shorten,
|
||||||
getCurrentUserFiles
|
getCurrentUserFiles
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
@ -22,6 +28,7 @@ import { useLocation } from 'react-router-dom'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {
|
import {
|
||||||
addMarks,
|
addMarks,
|
||||||
|
convertToPdfBlob,
|
||||||
FONT_SIZE,
|
FONT_SIZE,
|
||||||
FONT_TYPE,
|
FONT_TYPE,
|
||||||
groupMarksByFileNamePage,
|
groupMarksByFileNamePage,
|
||||||
@ -35,6 +42,9 @@ import { Container } from '../../components/Container'
|
|||||||
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
||||||
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
||||||
import { UsersDetails } from '../../components/UsersDetails.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'
|
||||||
import FileList from '../../components/FileList'
|
import FileList from '../../components/FileList'
|
||||||
import { CurrentUserFile } from '../../types/file.ts'
|
import { CurrentUserFile } from '../../types/file.ts'
|
||||||
import { Mark } from '../../types/mark.ts'
|
import { Mark } from '../../types/mark.ts'
|
||||||
@ -153,26 +163,12 @@ const SlimPdfView = ({
|
|||||||
|
|
||||||
export const VerifyPage = () => {
|
export const VerifyPage = () => {
|
||||||
const location = useLocation()
|
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
|
* 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
|
* meta will be received in navigation from create & home page in online mode
|
||||||
*/
|
*/
|
||||||
const { uploadedZip, meta: metaInNavState } = location.state || {}
|
const { uploadedZip, meta } = location.state || {}
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
||||||
useEffect(() => {
|
|
||||||
if (uploadedZip) {
|
|
||||||
setSelectedFile(uploadedZip)
|
|
||||||
}
|
|
||||||
}, [uploadedZip])
|
|
||||||
|
|
||||||
const [meta, setMeta] = useState<Meta>(metaInNavState)
|
|
||||||
const {
|
const {
|
||||||
submittedBy,
|
submittedBy,
|
||||||
zipUrl,
|
zipUrl,
|
||||||
@ -183,22 +179,44 @@ export const VerifyPage = () => {
|
|||||||
parsedSignatureEvents
|
parsedSignatureEvents
|
||||||
} = useSigitMeta(meta)
|
} = useSigitMeta(meta)
|
||||||
|
|
||||||
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
const profiles = useSigitProfiles([
|
||||||
|
...(submittedBy ? [submittedBy] : []),
|
||||||
|
...signers,
|
||||||
|
...viewers
|
||||||
|
])
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
|
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
|
|
||||||
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
|
|
||||||
const [currentFileHashes, setCurrentFileHashes] = useState<{
|
const [currentFileHashes, setCurrentFileHashes] = useState<{
|
||||||
[key: string]: string | null
|
[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(() => {
|
useEffect(() => {
|
||||||
if (Object.entries(files).length > 0) {
|
if (Object.entries(files).length > 0) {
|
||||||
const tmp = getCurrentUserFiles(files, currentFileHashes, fileHashes)
|
const tmp = getCurrentUserFiles(files, fileHashes, signatureFileHashes)
|
||||||
setCurrentFile(tmp[0])
|
setCurrentFile(tmp[0])
|
||||||
}
|
}
|
||||||
}, [currentFileHashes, fileHashes, files])
|
}, [signatureFileHashes, fileHashes, files])
|
||||||
|
|
||||||
|
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (metaInNavState && encryptionKey) {
|
if (uploadedZip) {
|
||||||
|
setSelectedFile(uploadedZip)
|
||||||
|
} else if (meta && encryptionKey) {
|
||||||
const processSigit = async () => {
|
const processSigit = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
@ -283,7 +301,7 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
processSigit()
|
processSigit()
|
||||||
}
|
}
|
||||||
}, [encryptionKey, metaInNavState, zipUrl])
|
}, [encryptionKey, meta, uploadedZip, zipUrl])
|
||||||
|
|
||||||
const handleVerify = async () => {
|
const handleVerify = async () => {
|
||||||
if (!selectedFile) return
|
if (!selectedFile) return
|
||||||
@ -297,7 +315,6 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
if (!zip) return
|
if (!zip) return
|
||||||
|
|
||||||
const files: { [filename: string]: SigitFile } = {}
|
|
||||||
const fileHashes: { [key: string]: string | null } = {}
|
const fileHashes: { [key: string]: string | null } = {}
|
||||||
const fileNames = Object.values(zip.files)
|
const fileNames = Object.values(zip.files)
|
||||||
.filter((entry) => entry.name.startsWith('files/') && !entry.dir)
|
.filter((entry) => entry.name.startsWith('files/') && !entry.dir)
|
||||||
@ -305,27 +322,24 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
// generate hashes for all entries in files folder of zipArchive
|
// generate hashes for all entries in files folder of zipArchive
|
||||||
// these hashes can be used to verify the originality of files
|
// these hashes can be used to verify the originality of files
|
||||||
for (const zipFilePath of fileNames) {
|
for (const fileName of fileNames) {
|
||||||
const arrayBuffer = await readContentOfZipEntry(
|
const arrayBuffer = await readContentOfZipEntry(
|
||||||
zip,
|
zip,
|
||||||
zipFilePath,
|
fileName,
|
||||||
'arraybuffer'
|
'arraybuffer'
|
||||||
)
|
)
|
||||||
|
|
||||||
const fileName = zipFilePath.replace(/^files\//, '')
|
|
||||||
if (arrayBuffer) {
|
if (arrayBuffer) {
|
||||||
files[fileName] = await convertToSigitFile(arrayBuffer, fileName)
|
|
||||||
const hash = await getHash(arrayBuffer)
|
const hash = await getHash(arrayBuffer)
|
||||||
|
|
||||||
if (hash) {
|
if (hash) {
|
||||||
fileHashes[fileName] = hash
|
fileHashes[fileName.replace(/^files\//, '')] = hash
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fileHashes[fileName] = null
|
fileHashes[fileName.replace(/^files\//, '')] = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setFiles(files)
|
|
||||||
setCurrentFileHashes(fileHashes)
|
setCurrentFileHashes(fileHashes)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Parsing meta.json')
|
setLoadingSpinnerDesc('Parsing meta.json')
|
||||||
@ -354,7 +368,43 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
if (!parsedMetaJson) return
|
if (!parsedMetaJson) return
|
||||||
|
|
||||||
setMeta(parsedMetaJson)
|
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
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
@ -398,7 +448,8 @@ export const VerifyPage = () => {
|
|||||||
for (const [fileName, file] of Object.entries(files)) {
|
for (const [fileName, file] of Object.entries(files)) {
|
||||||
if (file.isPdf) {
|
if (file.isPdf) {
|
||||||
// Draw marks into PDF file and generate a brand new blob
|
// Draw marks into PDF file and generate a brand new blob
|
||||||
const blob = await addMarks(file, marksByPage[fileName])
|
const pages = await addMarks(file, marksByPage[fileName])
|
||||||
|
const blob = await convertToPdfBlob(pages)
|
||||||
zip.file(`files/${fileName}`, blob)
|
zip.file(`files/${fileName}`, blob)
|
||||||
} else {
|
} else {
|
||||||
zip.file(`files/${fileName}`, file)
|
zip.file(`files/${fileName}`, file)
|
||||||
@ -428,6 +479,47 @@ export const VerifyPage = () => {
|
|||||||
setIsLoading(false)
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
@ -462,19 +554,22 @@ export const VerifyPage = () => {
|
|||||||
{meta && (
|
{meta && (
|
||||||
<StickySideColumns
|
<StickySideColumns
|
||||||
left={
|
left={
|
||||||
currentFile !== null && (
|
<>
|
||||||
<FileList
|
{currentFile !== null && (
|
||||||
files={getCurrentUserFiles(
|
<FileList
|
||||||
files,
|
files={getCurrentUserFiles(
|
||||||
currentFileHashes,
|
files,
|
||||||
fileHashes
|
currentFileHashes,
|
||||||
)}
|
signatureFileHashes
|
||||||
currentFile={currentFile}
|
)}
|
||||||
setCurrentFile={setCurrentFile}
|
currentFile={currentFile}
|
||||||
handleDownload={handleExport}
|
setCurrentFile={setCurrentFile}
|
||||||
downloadLabel="Download Sigit"
|
handleDownload={handleExport}
|
||||||
/>
|
downloadLabel="Download Sigit"
|
||||||
)
|
/>
|
||||||
|
)}
|
||||||
|
{displayExportedBy()}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
right={<UsersDetails meta={meta} />}
|
right={<UsersDetails meta={meta} />}
|
||||||
leftIcon={faFileDownload}
|
leftIcon={faFileDownload}
|
||||||
@ -483,7 +578,11 @@ export const VerifyPage = () => {
|
|||||||
>
|
>
|
||||||
<SlimPdfView
|
<SlimPdfView
|
||||||
currentFile={currentFile}
|
currentFile={currentFile}
|
||||||
files={getCurrentUserFiles(files, currentFileHashes, fileHashes)}
|
files={getCurrentUserFiles(
|
||||||
|
files,
|
||||||
|
currentFileHashes,
|
||||||
|
signatureFileHashes
|
||||||
|
)}
|
||||||
parsedSignatureEvents={parsedSignatureEvents}
|
parsedSignatureEvents={parsedSignatureEvents}
|
||||||
/>
|
/>
|
||||||
</StickySideColumns>
|
</StickySideColumns>
|
||||||
|
@ -61,6 +61,6 @@
|
|||||||
|
|
||||||
[data-dev='true'] {
|
[data-dev='true'] {
|
||||||
.mark {
|
.mark {
|
||||||
outline: 1px dotted black;
|
border: 1px dotted black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
export class TimeoutError extends Error {
|
|
||||||
constructor() {
|
|
||||||
super('Timeout')
|
|
||||||
this.name = this.constructor.name
|
|
||||||
}
|
|
||||||
}
|
|
@ -21,8 +21,6 @@ export const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000
|
|||||||
|
|
||||||
export const SIGIT_RELAY = 'wss://relay.sigit.io'
|
export const SIGIT_RELAY = 'wss://relay.sigit.io'
|
||||||
|
|
||||||
export const SIGIT_BLOSSOM = 'https://blossom.sigit.io'
|
|
||||||
|
|
||||||
export const DEFAULT_LOOK_UP_RELAY_LIST = [
|
export const DEFAULT_LOOK_UP_RELAY_LIST = [
|
||||||
SIGIT_RELAY,
|
SIGIT_RELAY,
|
||||||
'wss://user.kindpag.es',
|
'wss://user.kindpag.es',
|
||||||
|
@ -13,7 +13,7 @@ import { setRelayInfoAction } from '../store/actions'
|
|||||||
export const getNostrJoiningBlockNumber = async (
|
export const getNostrJoiningBlockNumber = async (
|
||||||
hexKey: string
|
hexKey: string
|
||||||
): Promise<NostrJoiningBlock | null> => {
|
): Promise<NostrJoiningBlock | null> => {
|
||||||
const metadataController = MetadataController.getInstance()
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
const relaySet = await metadataController.findRelayListMetadata(hexKey)
|
const relaySet = await metadataController.findRelayListMetadata(hexKey)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { MOST_COMMON_MEDIA_TYPES } from './const.ts'
|
|||||||
import { extractMarksFromSignedMeta } from './mark.ts'
|
import { extractMarksFromSignedMeta } from './mark.ts'
|
||||||
import {
|
import {
|
||||||
addMarks,
|
addMarks,
|
||||||
|
convertToPdfBlob,
|
||||||
groupMarksByFileNamePage,
|
groupMarksByFileNamePage,
|
||||||
isPdf,
|
isPdf,
|
||||||
pdfToImages
|
pdfToImages
|
||||||
@ -21,7 +22,8 @@ export const getZipWithFiles = async (
|
|||||||
for (const [fileName, file] of Object.entries(files)) {
|
for (const [fileName, file] of Object.entries(files)) {
|
||||||
if (file.isPdf) {
|
if (file.isPdf) {
|
||||||
// Handle PDF Files
|
// Handle PDF Files
|
||||||
const blob = await addMarks(file, marksByFileNamePage[fileName])
|
const pages = await addMarks(file, marksByFileNamePage[fileName])
|
||||||
|
const blob = await convertToPdfBlob(pages)
|
||||||
zip.file(`files/${fileName}`, blob)
|
zip.file(`files/${fileName}`, blob)
|
||||||
} else {
|
} else {
|
||||||
// Handle other files
|
// Handle other files
|
||||||
|
@ -3,27 +3,6 @@ import { hexToNpub } from './nostr.ts'
|
|||||||
import { Meta, SignedEventContent } from '../types'
|
import { Meta, SignedEventContent } from '../types'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { EMPTY } from './const.ts'
|
import { EMPTY } from './const.ts'
|
||||||
import { MarkType } from '../types/drawing.ts'
|
|
||||||
import {
|
|
||||||
faT,
|
|
||||||
faSignature,
|
|
||||||
faBriefcase,
|
|
||||||
faIdCard,
|
|
||||||
faHeading,
|
|
||||||
faClock,
|
|
||||||
faCalendarDays,
|
|
||||||
fa1,
|
|
||||||
faImage,
|
|
||||||
faSquareCheck,
|
|
||||||
faCheckDouble,
|
|
||||||
faPaperclip,
|
|
||||||
faCircleDot,
|
|
||||||
faSquareCaretDown,
|
|
||||||
faTableCellsLarge,
|
|
||||||
faStamp,
|
|
||||||
faCreditCard,
|
|
||||||
faPhone
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes in an array of Marks already filtered by User.
|
* Takes in an array of Marks already filtered by User.
|
||||||
@ -152,121 +131,6 @@ const findOtherUserMarks = (marks: Mark[], pubkey: string): Mark[] => {
|
|||||||
return marks.filter((mark) => mark.npub !== hexToNpub(pubkey))
|
return marks.filter((mark) => mark.npub !== hexToNpub(pubkey))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_TOOLBOX = [
|
|
||||||
{
|
|
||||||
identifier: MarkType.TEXT,
|
|
||||||
icon: faT,
|
|
||||||
label: 'Text',
|
|
||||||
active: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.SIGNATURE,
|
|
||||||
icon: faSignature,
|
|
||||||
label: 'Signature',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.JOBTITLE,
|
|
||||||
icon: faBriefcase,
|
|
||||||
label: 'Job Title',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.FULLNAME,
|
|
||||||
icon: faIdCard,
|
|
||||||
label: 'Full Name',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.INITIALS,
|
|
||||||
icon: faHeading,
|
|
||||||
label: 'Initials',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.DATETIME,
|
|
||||||
icon: faClock,
|
|
||||||
label: 'Date Time',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.DATE,
|
|
||||||
icon: faCalendarDays,
|
|
||||||
label: 'Date',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.NUMBER,
|
|
||||||
icon: fa1,
|
|
||||||
label: 'Number',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.IMAGES,
|
|
||||||
icon: faImage,
|
|
||||||
label: 'Images',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.CHECKBOX,
|
|
||||||
icon: faSquareCheck,
|
|
||||||
label: 'Checkbox',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.MULTIPLE,
|
|
||||||
icon: faCheckDouble,
|
|
||||||
label: 'Multiple',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.FILE,
|
|
||||||
icon: faPaperclip,
|
|
||||||
label: 'File',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.RADIO,
|
|
||||||
icon: faCircleDot,
|
|
||||||
label: 'Radio',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.SELECT,
|
|
||||||
icon: faSquareCaretDown,
|
|
||||||
label: 'Select',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.CELLS,
|
|
||||||
icon: faTableCellsLarge,
|
|
||||||
label: 'Cells',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.STAMP,
|
|
||||||
icon: faStamp,
|
|
||||||
label: 'Stamp',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.PAYMENT,
|
|
||||||
icon: faCreditCard,
|
|
||||||
label: 'Payment',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.PHONE,
|
|
||||||
icon: faPhone,
|
|
||||||
label: 'Phone',
|
|
||||||
active: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export const getToolboxLabelByMarkType = (markType: MarkType) => {
|
|
||||||
return DEFAULT_TOOLBOX.find((t) => t.identifier === markType)?.label
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getCurrentUserMarks,
|
getCurrentUserMarks,
|
||||||
filterMarksByPubkey,
|
filterMarksByPubkey,
|
||||||
|
@ -16,8 +16,6 @@ import { CreateSignatureEventContent, Meta } from '../types'
|
|||||||
import { hexToNpub, unixNow } from './nostr'
|
import { hexToNpub, unixNow } from './nostr'
|
||||||
import { parseJson } from './string'
|
import { parseJson } from './string'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
import { getHash } from './hash.ts'
|
|
||||||
import { SIGIT_BLOSSOM } from './const.ts'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a file to a file storage service.
|
* Uploads a file to a file storage service.
|
||||||
@ -27,18 +25,12 @@ import { SIGIT_BLOSSOM } from './const.ts'
|
|||||||
*/
|
*/
|
||||||
export const uploadToFileStorage = async (file: File) => {
|
export const uploadToFileStorage = async (file: File) => {
|
||||||
// Define event metadata for authorization
|
// Define event metadata for authorization
|
||||||
const hash = await getHash(await file.arrayBuffer())
|
|
||||||
if (!hash) {
|
|
||||||
throw new Error("Can't get file hash.")
|
|
||||||
}
|
|
||||||
|
|
||||||
const event: EventTemplate = {
|
const event: EventTemplate = {
|
||||||
kind: 24242,
|
kind: 24242,
|
||||||
content: 'Authorize Upload',
|
content: 'Authorize Upload',
|
||||||
created_at: unixNow(),
|
created_at: unixNow(),
|
||||||
tags: [
|
tags: [
|
||||||
['t', 'upload'],
|
['t', 'upload'],
|
||||||
['x', hash],
|
|
||||||
['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
|
['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
|
||||||
['name', file.name],
|
['name', file.name],
|
||||||
['size', String(file.size)]
|
['size', String(file.size)]
|
||||||
@ -55,8 +47,11 @@ export const uploadToFileStorage = async (file: File) => {
|
|||||||
// Sign the authorization event using the dedicated key stored in user app data
|
// Sign the authorization event using the dedicated key stored in user app data
|
||||||
const authEvent = finalizeEvent(event, hexToBytes(key))
|
const authEvent = finalizeEvent(event, hexToBytes(key))
|
||||||
|
|
||||||
|
// URL of the file storage service
|
||||||
|
const FILE_STORAGE_URL = 'https://blossom.sigit.io' // REFACTOR: should be an env
|
||||||
|
|
||||||
// Upload the file to the file storage service using Axios
|
// Upload the file to the file storage service using Axios
|
||||||
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
|
const response = await axios.put(`${FILE_STORAGE_URL}/upload`, file, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)), // Set authorization header
|
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)), // Set authorization header
|
||||||
'Content-Type': 'application/sigit' // Set content type header
|
'Content-Type': 'application/sigit' // Set content type header
|
||||||
|
@ -35,7 +35,6 @@ import { getDefaultRelayMap } from './relays'
|
|||||||
import { parseJson, removeLeadingSlash } from './string'
|
import { parseJson, removeLeadingSlash } from './string'
|
||||||
import { timeout } from './utils'
|
import { timeout } from './utils'
|
||||||
import { getHash } from './hash'
|
import { getHash } from './hash'
|
||||||
import { SIGIT_BLOSSOM } from './const.ts'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a `d` tag for userAppData
|
* Generates a `d` tag for userAppData
|
||||||
@ -366,7 +365,7 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
|
|||||||
// Check if relayMap is undefined in the Redux store
|
// Check if relayMap is undefined in the Redux store
|
||||||
if (!relayMap) {
|
if (!relayMap) {
|
||||||
// If relayMap is not present, fetch relay list metadata
|
// If relayMap is not present, fetch relay list metadata
|
||||||
const metadataController = MetadataController.getInstance()
|
const metadataController = new MetadataController()
|
||||||
const relaySet = await metadataController
|
const relaySet = await metadataController
|
||||||
.findRelayListMetadata(usersPubkey)
|
.findRelayListMetadata(usersPubkey)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -724,11 +723,6 @@ const uploadUserAppDataToBlossom = async (
|
|||||||
type: 'application/octet-stream'
|
type: 'application/octet-stream'
|
||||||
})
|
})
|
||||||
|
|
||||||
const hash = await getHash(await file.arrayBuffer())
|
|
||||||
if (!hash) {
|
|
||||||
throw new Error("Can't get file hash.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define event metadata for authorization
|
// Define event metadata for authorization
|
||||||
const event: EventTemplate = {
|
const event: EventTemplate = {
|
||||||
kind: 24242,
|
kind: 24242,
|
||||||
@ -736,7 +730,6 @@ const uploadUserAppDataToBlossom = async (
|
|||||||
created_at: unixNow(),
|
created_at: unixNow(),
|
||||||
tags: [
|
tags: [
|
||||||
['t', 'upload'],
|
['t', 'upload'],
|
||||||
['x', hash],
|
|
||||||
['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
|
['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
|
||||||
['name', file.name],
|
['name', file.name],
|
||||||
['size', String(file.size)]
|
['size', String(file.size)]
|
||||||
@ -746,8 +739,11 @@ const uploadUserAppDataToBlossom = async (
|
|||||||
// Finalize the event with the private key
|
// Finalize the event with the private key
|
||||||
const authEvent = finalizeEvent(event, hexToBytes(privateKey))
|
const authEvent = finalizeEvent(event, hexToBytes(privateKey))
|
||||||
|
|
||||||
|
// URL of the file storage service
|
||||||
|
const FILE_STORAGE_URL = 'https://blossom.sigit.io'
|
||||||
|
|
||||||
// Upload the file to the file storage service using Axios
|
// Upload the file to the file storage service using Axios
|
||||||
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
|
const response = await axios.put(`${FILE_STORAGE_URL}/upload`, file, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header
|
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header
|
||||||
}
|
}
|
||||||
@ -839,7 +835,7 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
|
|||||||
*/
|
*/
|
||||||
export const subscribeForSigits = async (pubkey: string) => {
|
export const subscribeForSigits = async (pubkey: string) => {
|
||||||
// Instantiate the MetadataController to retrieve relay list metadata
|
// Instantiate the MetadataController to retrieve relay list metadata
|
||||||
const metadataController = MetadataController.getInstance()
|
const metadataController = new MetadataController()
|
||||||
const relaySet = await metadataController
|
const relaySet = await metadataController
|
||||||
.findRelayListMetadata(pubkey)
|
.findRelayListMetadata(pubkey)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -943,7 +939,7 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
|
|||||||
const wrappedEvent = createWrap(unsignedEvent, receiver)
|
const wrappedEvent = createWrap(unsignedEvent, receiver)
|
||||||
|
|
||||||
// Instantiate the MetadataController to retrieve relay list metadata
|
// Instantiate the MetadataController to retrieve relay list metadata
|
||||||
const metadataController = MetadataController.getInstance()
|
const metadataController = new MetadataController()
|
||||||
const relaySet = await metadataController
|
const relaySet = await metadataController
|
||||||
.findRelayListMetadata(receiver)
|
.findRelayListMetadata(receiver)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
120
src/utils/pdf.ts
120
src/utils/pdf.ts
@ -1,6 +1,7 @@
|
|||||||
import { PdfPage } from '../types/drawing.ts'
|
import { PdfPage } from '../types/drawing.ts'
|
||||||
import { PDFDocument, PDFFont, PDFPage, rgb } from 'pdf-lib'
|
import { PDFDocument } from 'pdf-lib'
|
||||||
import { Mark } from '../types/mark.ts'
|
import { Mark } from '../types/mark.ts'
|
||||||
|
|
||||||
import * as PDFJS from 'pdfjs-dist'
|
import * as PDFJS from 'pdfjs-dist'
|
||||||
import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker'
|
import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker'
|
||||||
if (!PDFJS.GlobalWorkerOptions.workerPort) {
|
if (!PDFJS.GlobalWorkerOptions.workerPort) {
|
||||||
@ -9,23 +10,18 @@ if (!PDFJS.GlobalWorkerOptions.workerPort) {
|
|||||||
PDFJS.GlobalWorkerOptions.workerPort = worker
|
PDFJS.GlobalWorkerOptions.workerPort = worker
|
||||||
}
|
}
|
||||||
|
|
||||||
import fontkit from '@pdf-lib/fontkit'
|
|
||||||
import defaultFont from '../assets/fonts/roboto-regular.ttf'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defined font size used when generating a PDF. Currently it is difficult to fully
|
* Defined font size used when generating a PDF. Currently it is difficult to fully
|
||||||
* correlate font size used at the time of filling in / drawing on the PDF
|
* correlate font size used at the time of filling in / drawing on the PDF
|
||||||
* because it is dynamically rendered, and the final size.
|
* because it is dynamically rendered, and the final size.
|
||||||
|
* This should be fixed going forward.
|
||||||
|
* Switching to PDF-Lib will most likely make this problem redundant.
|
||||||
*/
|
*/
|
||||||
export const FONT_SIZE: number = 16
|
export const FONT_SIZE: number = 16
|
||||||
/**
|
/**
|
||||||
* Current font type used when generating a PDF.
|
* Current font type used when generating a PDF.
|
||||||
*/
|
*/
|
||||||
export const FONT_TYPE: string = 'Roboto'
|
export const FONT_TYPE: string = 'Arial'
|
||||||
/**
|
|
||||||
* Current line height used when generating a PDF.
|
|
||||||
*/
|
|
||||||
export const FONT_LINE_HEIGHT: number = 1
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility that transforms a drawing coordinate number into a CSS-compatible pixel string
|
* A utility that transforms a drawing coordinate number into a CSS-compatible pixel string
|
||||||
@ -119,28 +115,56 @@ export const pdfToImages = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes in individual pdf file and an object with Marks grouped by Page number
|
* Takes in individual pdf file and an object with Marks grouped by Page number
|
||||||
* Returns a PDF blob with embedded, completed and signed marks from all users as text
|
* Returns an array of encoded images where each image is a representation
|
||||||
|
* of a PDF page with completed and signed marks from all users
|
||||||
*/
|
*/
|
||||||
export const addMarks = async (
|
export const addMarks = async (
|
||||||
file: File,
|
file: File,
|
||||||
marksPerPage: { [key: string]: Mark[] }
|
marksPerPage: { [key: string]: Mark[] }
|
||||||
) => {
|
) => {
|
||||||
const p = await readPdf(file)
|
const p = await readPdf(file)
|
||||||
const pdf = await PDFDocument.load(p)
|
const pdf = await PDFJS.getDocument(p).promise
|
||||||
const robotoFont = await embedFont(pdf)
|
const canvas = document.createElement('canvas')
|
||||||
const pages = pdf.getPages()
|
|
||||||
|
|
||||||
for (let i = 0; i < pages.length; i++) {
|
const images: string[] = []
|
||||||
if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
|
|
||||||
marksPerPage[i]?.forEach((mark) =>
|
for (let i = 0; i < pdf.numPages; i++) {
|
||||||
drawMarkText(mark, pages[i], robotoFont)
|
const page = await pdf.getPage(i + 1)
|
||||||
)
|
const viewport = page.getViewport({ scale: 1 })
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
canvas.height = viewport.height
|
||||||
|
canvas.width = viewport.width
|
||||||
|
if (context) {
|
||||||
|
await page.render({ canvasContext: context, viewport: viewport }).promise
|
||||||
|
|
||||||
|
if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
|
||||||
|
marksPerPage[i]?.forEach((mark) => draw(mark, context))
|
||||||
|
}
|
||||||
|
|
||||||
|
images.push(canvas.toDataURL())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = await pdf.save()
|
canvas.remove()
|
||||||
|
|
||||||
return blob
|
return images
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to scale mark in line with the PDF-to-PNG scale
|
||||||
|
*/
|
||||||
|
export const scaleMark = (mark: Mark, scale: number): Mark => {
|
||||||
|
const { location } = mark
|
||||||
|
return {
|
||||||
|
...mark,
|
||||||
|
location: {
|
||||||
|
...location,
|
||||||
|
width: location.width * scale,
|
||||||
|
height: location.height * scale,
|
||||||
|
left: location.left * scale,
|
||||||
|
top: location.top * scale
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -153,7 +177,6 @@ export const hasValue = (mark: Mark): boolean => !!mark.value
|
|||||||
* Draws a Mark on a Canvas representation of a PDF Page
|
* Draws a Mark on a Canvas representation of a PDF Page
|
||||||
* @param mark to be drawn
|
* @param mark to be drawn
|
||||||
* @param ctx a Canvas representation of a specific PDF Page
|
* @param ctx a Canvas representation of a specific PDF Page
|
||||||
* @deprecated use drawMarkText
|
|
||||||
*/
|
*/
|
||||||
export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
|
export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
|
||||||
const { location } = mark
|
const { location } = mark
|
||||||
@ -168,37 +191,27 @@ export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws a Mark on a PDF Page
|
* Takes an array of encoded PDF pages and returns a blob that is a complete PDF file
|
||||||
* @param mark to be drawn
|
* @param markedPdfPages
|
||||||
* @param page PDF Page
|
|
||||||
* @param font embedded font
|
|
||||||
*/
|
*/
|
||||||
export const drawMarkText = (mark: Mark, page: PDFPage, font: PDFFont) => {
|
export const convertToPdfBlob = async (
|
||||||
const { location } = mark
|
markedPdfPages: string[]
|
||||||
const { height } = page.getSize()
|
): Promise<Blob> => {
|
||||||
|
const pdfDoc = await PDFDocument.create()
|
||||||
|
|
||||||
// Convert the mark location origin (top, left) to PDF origin (bottom, left)
|
for (const page of markedPdfPages) {
|
||||||
const x = location.left
|
const pngImage = await pdfDoc.embedPng(page)
|
||||||
const y = height - location.top
|
const p = pdfDoc.addPage([pngImage.width, pngImage.height])
|
||||||
|
p.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: pngImage.width,
|
||||||
|
height: pngImage.height
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Adjust y-coordinate for the text, drawText's y is the baseline for the font
|
const pdfBytes = await pdfDoc.save()
|
||||||
// We start from the y (top location border) and we need to bump it down
|
return new Blob([pdfBytes], { type: 'application/pdf' })
|
||||||
// We move font baseline by the difference between rendered height and actual height (gap)
|
|
||||||
// And finally move down by the height without descender to get the new baseline
|
|
||||||
const adjustedY =
|
|
||||||
y -
|
|
||||||
(font.heightAtSize(FONT_SIZE) - FONT_SIZE) -
|
|
||||||
font.heightAtSize(FONT_SIZE, { descender: false })
|
|
||||||
|
|
||||||
page.drawText(`${mark.value}`, {
|
|
||||||
x,
|
|
||||||
y: adjustedY,
|
|
||||||
size: FONT_SIZE,
|
|
||||||
font: font,
|
|
||||||
color: rgb(0, 0, 0),
|
|
||||||
maxWidth: location.width,
|
|
||||||
lineHeight: FONT_SIZE * FONT_LINE_HEIGHT
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -236,12 +249,3 @@ export const byPage = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function embedFont(pdf: PDFDocument) {
|
|
||||||
const fontBytes = await fetch(defaultFont).then((res) => res.arrayBuffer())
|
|
||||||
|
|
||||||
pdf.registerFontkit(fontkit)
|
|
||||||
|
|
||||||
const embeddedFont = await pdf.embedFont(fontBytes)
|
|
||||||
return embeddedFont
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { TimeoutError } from '../types/errors/TimeoutError.ts'
|
|
||||||
import { CurrentUserFile } from '../types/file.ts'
|
import { CurrentUserFile } from '../types/file.ts'
|
||||||
import { SigitFile } from './file.ts'
|
import { SigitFile } from './file.ts'
|
||||||
|
|
||||||
@ -35,7 +34,7 @@ export const isOnline = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Define a URL to check the online status
|
// Define a URL to check the online status
|
||||||
const url = document.location.pathname + '?v=' + new Date().getTime()
|
const url = 'https://www.google.com'
|
||||||
|
|
||||||
// Make a HEAD request to the URL with 'no-cors' mode
|
// 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
|
// This mode is used to handle opaque responses which do not expose their content
|
||||||
@ -64,7 +63,7 @@ export const timeout = (ms: number = 60000) => {
|
|||||||
// Set a timeout using setTimeout
|
// Set a timeout using setTimeout
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Reject the promise with an Error indicating a timeout
|
// Reject the promise with an Error indicating a timeout
|
||||||
reject(new TimeoutError())
|
reject(new Error('Timeout'))
|
||||||
}, ms) // Timeout duration in milliseconds
|
}, ms) // Timeout duration in milliseconds
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user