Compare commits
27 Commits
b92790ceed
...
3d5006a715
Author | SHA1 | Date | |
---|---|---|---|
3d5006a715 | |||
f38344b9ac | |||
2b630c94b6 | |||
edeb22fb37 | |||
a2138f1de1 | |||
85bf907f54 | |||
3b447dcf6a | |||
21aa25a42a | |||
edbe708b65 | |||
7056ad3cd3 | |||
7dffe75bd7 | |||
8da2510a18 | |||
633c23e459 | |||
2e1d48168a | |||
e05d3e53a2 | |||
d8d51be603 | |||
5f92906032 | |||
70cca9dd10 | |||
9bae5b9ba2 | |||
a1bf88d243 | |||
67c3c74515 | |||
f81f2b0523 | |||
182ef40d8d | |||
39934f59c3 | |||
dd97dfbaf0 | |||
dfdcb8419d | |||
f8a4480994 |
@ -28,9 +28,9 @@ import {
|
|||||||
} from '../../routes'
|
} from '../../routes'
|
||||||
import {
|
import {
|
||||||
clearAuthToken,
|
clearAuthToken,
|
||||||
|
getProfileUsername,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
saveNsecBunkerDelegatedKey,
|
saveNsecBunkerDelegatedKey
|
||||||
shorten
|
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { setUserRobotImage } from '../../store/userRobotImage/action'
|
import { setUserRobotImage } from '../../store/userRobotImage/action'
|
||||||
@ -58,9 +58,8 @@ export const AppBar = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (metadataState) {
|
if (metadataState) {
|
||||||
if (metadataState.content) {
|
if (metadataState.content) {
|
||||||
const { picture, display_name, name } = JSON.parse(
|
const profileMetadata = JSON.parse(metadataState.content)
|
||||||
metadataState.content
|
const { picture } = profileMetadata
|
||||||
)
|
|
||||||
|
|
||||||
if (picture || userRobotImage) {
|
if (picture || userRobotImage) {
|
||||||
setUserAvatar(picture || userRobotImage)
|
setUserAvatar(picture || userRobotImage)
|
||||||
@ -70,7 +69,7 @@ export const AppBar = () => {
|
|||||||
? hexToNpub(authState.usersPubkey)
|
? hexToNpub(authState.usersPubkey)
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
setUsername(shorten(display_name || name || npub, 7))
|
setUsername(getProfileUsername(npub, profileMetadata))
|
||||||
} else {
|
} else {
|
||||||
setUserAvatar(userRobotImage || '')
|
setUserAvatar(userRobotImage || '')
|
||||||
setUsername('')
|
setUsername('')
|
||||||
@ -133,8 +132,10 @@ export const AppBar = () => {
|
|||||||
<div className={styles.banner}>
|
<div className={styles.banner}>
|
||||||
<Container>
|
<Container>
|
||||||
<div className={styles.bannerInner}>
|
<div className={styles.bannerInner}>
|
||||||
|
<p className={styles.bannerText}>
|
||||||
SIGit is currently Alpha software (available for internal
|
SIGit is currently Alpha software (available for internal
|
||||||
testing), use at your own risk!
|
testing), use at your own risk!
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
aria-label={`close banner`}
|
aria-label={`close banner`}
|
||||||
variant="text"
|
variant="text"
|
||||||
|
@ -67,3 +67,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bannerText {
|
||||||
|
margin-left: 54px;
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@ -11,16 +11,16 @@ import styles from './style.module.scss'
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { ProfileMetadata, User, UserRole } from '../../types'
|
import { ProfileMetadata, User, UserRole } from '../../types'
|
||||||
import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
|
import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
|
||||||
import { truncate } from 'lodash'
|
import { hexToNpub, npubToHex, getProfileUsername } from '../../utils'
|
||||||
import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils'
|
import { SigitFile } from '../../utils/file'
|
||||||
import { getSigitFile, SigitFile } from '../../utils/file'
|
|
||||||
import { getToolboxLabelByMarkType } from '../../utils/mark'
|
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 { FONT_SIZE, FONT_TYPE, 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 { UserAvatar } from '../UserAvatar'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
const DEFAULT_START_SIZE = {
|
const DEFAULT_START_SIZE = {
|
||||||
width: 140,
|
width: 140,
|
||||||
@ -28,52 +28,50 @@ const DEFAULT_START_SIZE = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedFiles: File[]
|
|
||||||
users: User[]
|
users: User[]
|
||||||
metadata: { [key: string]: ProfileMetadata }
|
metadata: { [key: string]: ProfileMetadata }
|
||||||
onDrawFieldsChange: (sigitFiles: SigitFile[]) => void
|
sigitFiles: SigitFile[]
|
||||||
|
setSigitFiles: React.Dispatch<React.SetStateAction<SigitFile[]>>
|
||||||
selectedTool?: DrawTool
|
selectedTool?: DrawTool
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DrawPDFFields = (props: Props) => {
|
export const DrawPDFFields = (props: Props) => {
|
||||||
const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props
|
const { selectedTool, sigitFiles, setSigitFiles, users } = props
|
||||||
const { to, from } = useScale()
|
|
||||||
|
|
||||||
const [sigitFiles, setSigitFiles] = useState<SigitFile[]>([])
|
const signers = users.filter((u) => u.role === UserRole.signer)
|
||||||
const [parsingPdf, setIsParsing] = useState<boolean>(false)
|
const defaultSignerNpub = signers.length ? hexToNpub(signers[0].pubkey) : ''
|
||||||
|
const [lastSigner, setLastSigner] = useState(defaultSignerNpub)
|
||||||
|
/**
|
||||||
|
* Return first pubkey that is present in the signers list
|
||||||
|
* @param pubkeys
|
||||||
|
* @returns available pubkey or empty string
|
||||||
|
*/
|
||||||
|
const getAvailableSigner = (...pubkeys: string[]) => {
|
||||||
|
const availableSigner: string | undefined = pubkeys.find((pubkey) =>
|
||||||
|
signers.some((s) => s.pubkey === npubToHex(pubkey))
|
||||||
|
)
|
||||||
|
return availableSigner || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const { to, from } = useScale()
|
||||||
|
|
||||||
const [mouseState, setMouseState] = useState<MouseState>({
|
const [mouseState, setMouseState] = useState<MouseState>({
|
||||||
clicked: false
|
clicked: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const [activeDrawField, setActiveDrawField] = useState<number>()
|
const [activeDrawnField, setActiveDrawnField] = useState<{
|
||||||
|
fileIndex: number
|
||||||
useEffect(() => {
|
pageIndex: number
|
||||||
if (selectedFiles) {
|
drawnFieldIndex: number
|
||||||
/**
|
}>()
|
||||||
* Reads the binary files and converts to internal file type
|
const isActiveDrawnField = (
|
||||||
* and sets to a state (adds images if it's a PDF)
|
fileIndex: number,
|
||||||
*/
|
pageIndex: number,
|
||||||
const parsePages = async () => {
|
drawnFieldIndex: number
|
||||||
const files = await settleAllFullfilfedPromises(
|
) =>
|
||||||
selectedFiles,
|
activeDrawnField?.fileIndex === fileIndex &&
|
||||||
getSigitFile
|
activeDrawnField?.pageIndex === pageIndex &&
|
||||||
)
|
activeDrawnField?.drawnFieldIndex === drawnFieldIndex
|
||||||
|
|
||||||
setSigitFiles(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsParsing(true)
|
|
||||||
|
|
||||||
parsePages().finally(() => {
|
|
||||||
setIsParsing(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [selectedFiles])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (sigitFiles) onDrawFieldsChange(sigitFiles)
|
|
||||||
}, [onDrawFieldsChange, sigitFiles])
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drawing events
|
* Drawing events
|
||||||
@ -100,7 +98,12 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* @param event Pointer event
|
* @param event Pointer event
|
||||||
* @param page PdfPage where press happened
|
* @param page PdfPage where press happened
|
||||||
*/
|
*/
|
||||||
const handlePointerDown = (event: React.PointerEvent, page: PdfPage) => {
|
const handlePointerDown = (
|
||||||
|
event: React.PointerEvent,
|
||||||
|
page: PdfPage,
|
||||||
|
fileIndex: number,
|
||||||
|
pageIndex: number
|
||||||
|
) => {
|
||||||
// Proceed only if left click
|
// Proceed only if left click
|
||||||
if (event.button !== 0) return
|
if (event.button !== 0) return
|
||||||
|
|
||||||
@ -115,12 +118,17 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
top: to(page.width, y),
|
top: to(page.width, y),
|
||||||
width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width,
|
width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width,
|
||||||
height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height,
|
height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height,
|
||||||
counterpart: '',
|
counterpart: getAvailableSigner(lastSigner, defaultSignerNpub),
|
||||||
type: selectedTool.identifier
|
type: selectedTool.identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
page.drawnFields.push(newField)
|
page.drawnFields.push(newField)
|
||||||
|
|
||||||
|
setActiveDrawnField({
|
||||||
|
fileIndex,
|
||||||
|
pageIndex,
|
||||||
|
drawnFieldIndex: page.drawnFields.length - 1
|
||||||
|
})
|
||||||
setMouseState((prev) => {
|
setMouseState((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
@ -189,6 +197,8 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
*/
|
*/
|
||||||
const handleDrawnFieldPointerDown = (
|
const handleDrawnFieldPointerDown = (
|
||||||
event: React.PointerEvent,
|
event: React.PointerEvent,
|
||||||
|
fileIndex: number,
|
||||||
|
pageIndex: number,
|
||||||
drawnFieldIndex: number
|
drawnFieldIndex: number
|
||||||
) => {
|
) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@ -198,7 +208,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
|
|
||||||
const drawingRectangleCoords = getPointerCoordinates(event)
|
const drawingRectangleCoords = getPointerCoordinates(event)
|
||||||
|
|
||||||
setActiveDrawField(drawnFieldIndex)
|
setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex })
|
||||||
setMouseState({
|
setMouseState({
|
||||||
dragging: true,
|
dragging: true,
|
||||||
clicked: false,
|
clicked: false,
|
||||||
@ -254,13 +264,15 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
*/
|
*/
|
||||||
const handleResizePointerDown = (
|
const handleResizePointerDown = (
|
||||||
event: React.PointerEvent,
|
event: React.PointerEvent,
|
||||||
|
fileIndex: number,
|
||||||
|
pageIndex: number,
|
||||||
drawnFieldIndex: number
|
drawnFieldIndex: number
|
||||||
) => {
|
) => {
|
||||||
// Proceed only if left click
|
// Proceed only if left click
|
||||||
if (event.button !== 0) return
|
if (event.button !== 0) return
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
setActiveDrawField(drawnFieldIndex)
|
setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex })
|
||||||
setMouseState({
|
setMouseState({
|
||||||
resizing: true
|
resizing: true
|
||||||
})
|
})
|
||||||
@ -369,7 +381,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
handlePointerMove(event, page)
|
handlePointerMove(event, page)
|
||||||
}}
|
}}
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
handlePointerDown(event, page)
|
handlePointerDown(event, page, fileIndex, pageIndex)
|
||||||
}}
|
}}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
src={page.image}
|
src={page.image}
|
||||||
@ -381,7 +393,12 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
<div
|
<div
|
||||||
key={drawnFieldIndex}
|
key={drawnFieldIndex}
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
handleDrawnFieldPointerDown(event, drawnFieldIndex)
|
handleDrawnFieldPointerDown(
|
||||||
|
event,
|
||||||
|
fileIndex,
|
||||||
|
pageIndex,
|
||||||
|
drawnFieldIndex
|
||||||
|
)
|
||||||
}
|
}
|
||||||
onPointerMove={(event) => {
|
onPointerMove={(event) => {
|
||||||
handleDrawnFieldPointerMove(event, drawnField, page.width)
|
handleDrawnFieldPointerMove(event, drawnField, page.width)
|
||||||
@ -402,7 +419,11 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
touchAction: 'none',
|
touchAction: 'none',
|
||||||
opacity:
|
opacity:
|
||||||
mouseState.dragging &&
|
mouseState.dragging &&
|
||||||
activeDrawField === drawnFieldIndex
|
isActiveDrawnField(
|
||||||
|
fileIndex,
|
||||||
|
pageIndex,
|
||||||
|
drawnFieldIndex
|
||||||
|
)
|
||||||
? 0.8
|
? 0.8
|
||||||
: undefined
|
: undefined
|
||||||
}}
|
}}
|
||||||
@ -419,7 +440,12 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
handleResizePointerDown(event, drawnFieldIndex)
|
handleResizePointerDown(
|
||||||
|
event,
|
||||||
|
fileIndex,
|
||||||
|
pageIndex,
|
||||||
|
drawnFieldIndex
|
||||||
|
)
|
||||||
}
|
}
|
||||||
onPointerMove={(event) => {
|
onPointerMove={(event) => {
|
||||||
handleResizePointerMove(event, drawnField, page.width)
|
handleResizePointerMove(event, drawnField, page.width)
|
||||||
@ -428,7 +454,11 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
mouseState.resizing &&
|
mouseState.resizing &&
|
||||||
activeDrawField === drawnFieldIndex
|
isActiveDrawnField(
|
||||||
|
fileIndex,
|
||||||
|
pageIndex,
|
||||||
|
drawnFieldIndex
|
||||||
|
)
|
||||||
? 'var(--primary-main)'
|
? 'var(--primary-main)'
|
||||||
: undefined
|
: undefined
|
||||||
}}
|
}}
|
||||||
@ -446,6 +476,23 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<Close fontSize="small" />
|
<Close fontSize="small" />
|
||||||
</span>
|
</span>
|
||||||
|
{!isActiveDrawnField(
|
||||||
|
fileIndex,
|
||||||
|
pageIndex,
|
||||||
|
drawnFieldIndex
|
||||||
|
) &&
|
||||||
|
!!drawnField.counterpart && (
|
||||||
|
<div className={styles.counterpartAvatar}>
|
||||||
|
<UserAvatar
|
||||||
|
pubkey={npubToHex(drawnField.counterpart)!}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isActiveDrawnField(
|
||||||
|
fileIndex,
|
||||||
|
pageIndex,
|
||||||
|
drawnFieldIndex
|
||||||
|
) && (
|
||||||
<div
|
<div
|
||||||
onPointerDown={handleUserSelectPointerDown}
|
onPointerDown={handleUserSelectPointerDown}
|
||||||
className={styles.userSelect}
|
className={styles.userSelect}
|
||||||
@ -453,9 +500,10 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
<FormControl fullWidth size="small">
|
<FormControl fullWidth size="small">
|
||||||
<InputLabel id="counterparts">Counterpart</InputLabel>
|
<InputLabel id="counterparts">Counterpart</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={drawnField.counterpart}
|
value={getAvailableSigner(drawnField.counterpart)}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
drawnField.counterpart = event.target.value
|
drawnField.counterpart = event.target.value
|
||||||
|
setLastSigner(event.target.value)
|
||||||
refreshPdfFiles()
|
refreshPdfFiles()
|
||||||
}}
|
}}
|
||||||
labelId="counterparts"
|
labelId="counterparts"
|
||||||
@ -463,39 +511,24 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
sx={{
|
sx={{
|
||||||
background: 'white'
|
background: 'white'
|
||||||
}}
|
}}
|
||||||
renderValue={(value) => renderCounterpartValue(value)}
|
renderValue={(value) =>
|
||||||
|
renderCounterpartValue(value)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{users
|
{signers.map((signer, index) => {
|
||||||
.filter((u) => u.role === UserRole.signer)
|
const npub = hexToNpub(signer.pubkey)
|
||||||
.map((user, index) => {
|
const metadata = props.metadata[signer.pubkey]
|
||||||
const npub = hexToNpub(user.pubkey)
|
const displayValue = getProfileUsername(
|
||||||
let displayValue = truncate(npub, {
|
|
||||||
length: 16
|
|
||||||
})
|
|
||||||
|
|
||||||
const metadata = props.metadata[user.pubkey]
|
|
||||||
|
|
||||||
if (metadata) {
|
|
||||||
displayValue = truncate(
|
|
||||||
metadata.name ||
|
|
||||||
metadata.display_name ||
|
|
||||||
metadata.username ||
|
|
||||||
npub,
|
npub,
|
||||||
{
|
metadata
|
||||||
length: 16
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem key={index} value={npub}>
|
||||||
key={index}
|
|
||||||
value={hexToNpub(user.pubkey)}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<AvatarIconButton
|
<AvatarIconButton
|
||||||
src={metadata?.picture}
|
src={metadata?.picture}
|
||||||
hexKey={user.pubkey}
|
hexKey={signer.pubkey}
|
||||||
aria-label={`account of user ${displayValue}`}
|
aria-label={`account of user ${displayValue}`}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
sx={{
|
sx={{
|
||||||
@ -514,6 +547,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -524,28 +558,19 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderCounterpartValue = (value: string) => {
|
const renderCounterpartValue = (npub: string) => {
|
||||||
const user = users.find((u) => u.pubkey === npubToHex(value))
|
let displayValue = _.truncate(npub, { length: 16 })
|
||||||
if (user) {
|
|
||||||
let displayValue = truncate(value, {
|
|
||||||
length: 16
|
|
||||||
})
|
|
||||||
|
|
||||||
const metadata = props.metadata[user.pubkey]
|
const signer = signers.find((u) => u.pubkey === npubToHex(npub))
|
||||||
|
if (signer) {
|
||||||
|
const metadata = props.metadata[signer.pubkey]
|
||||||
|
displayValue = getProfileUsername(npub, metadata)
|
||||||
|
|
||||||
if (metadata) {
|
|
||||||
displayValue = truncate(
|
|
||||||
metadata.name || metadata.display_name || metadata.username || value,
|
|
||||||
{
|
|
||||||
length: 16
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.counterpartSelectValue}>
|
||||||
<AvatarIconButton
|
<AvatarIconButton
|
||||||
src={props.metadata[user.pubkey]?.picture}
|
src={props.metadata[signer.pubkey]?.picture}
|
||||||
hexKey={npubToHex(user.pubkey) || undefined}
|
hexKey={signer.pubkey || undefined}
|
||||||
sx={{
|
sx={{
|
||||||
padding: 0,
|
padding: 0,
|
||||||
marginRight: '6px',
|
marginRight: '6px',
|
||||||
@ -556,19 +581,11 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{displayValue}
|
{displayValue}
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return displayValue
|
||||||
}
|
|
||||||
|
|
||||||
if (parsingPdf) {
|
|
||||||
return <LoadingSpinner variant="small" />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sigitFiles.length) {
|
|
||||||
return ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -589,7 +606,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
<ExtensionFileBox extension={file.extension} />
|
<ExtensionFileBox extension={file.extension} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{i < selectedFiles.length - 1 && <FileDivider />}
|
{i < sigitFiles.length - 1 && <FileDivider />}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -84,3 +84,14 @@
|
|||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.counterpartSelectValue {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counterpartAvatar {
|
||||||
|
img {
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { AvatarIconButton } from '../UserAvatarIconButton'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { useProfileMetadata } from '../../hooks/useProfileMetadata'
|
import { useProfileMetadata } from '../../hooks/useProfileMetadata'
|
||||||
import { Tooltip } from '@mui/material'
|
import { Tooltip } from '@mui/material'
|
||||||
import { shorten } from '../../utils'
|
import { getProfileUsername } from '../../utils'
|
||||||
import { TooltipChild } from '../TooltipChild'
|
import { TooltipChild } from '../TooltipChild'
|
||||||
|
|
||||||
interface UserAvatarProps {
|
interface UserAvatarProps {
|
||||||
@ -22,7 +22,7 @@ export const UserAvatar = ({
|
|||||||
isNameVisible = false
|
isNameVisible = false
|
||||||
}: UserAvatarProps) => {
|
}: UserAvatarProps) => {
|
||||||
const profile = useProfileMetadata(pubkey)
|
const profile = useProfileMetadata(pubkey)
|
||||||
const name = profile?.display_name || profile?.name || shorten(pubkey)
|
const name = getProfileUsername(pubkey, profile)
|
||||||
const image = profile?.picture
|
const image = profile?.picture
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -3,7 +3,8 @@ import {
|
|||||||
CreateSignatureEventContent,
|
CreateSignatureEventContent,
|
||||||
DocSignatureEvent,
|
DocSignatureEvent,
|
||||||
Meta,
|
Meta,
|
||||||
SignedEventContent
|
SignedEventContent,
|
||||||
|
OpenTimestamp
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { Mark } from '../types/mark'
|
import { Mark } from '../types/mark'
|
||||||
import {
|
import {
|
||||||
@ -59,6 +60,8 @@ export interface FlatMeta
|
|||||||
signersStatus: {
|
signersStatus: {
|
||||||
[signer: `npub1${string}`]: SignStatus
|
[signer: `npub1${string}`]: SignStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timestamps?: OpenTimestamp[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,7 +166,6 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
setEncryptionKey(decrypted)
|
setEncryptionKey(decrypted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temp. map to hold events and signers
|
// Temp. map to hold events and signers
|
||||||
const parsedSignatureEventsMap = new Map<
|
const parsedSignatureEventsMap = new Map<
|
||||||
`npub1${string}`,
|
`npub1${string}`,
|
||||||
@ -277,6 +279,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
createSignature: meta?.createSignature,
|
createSignature: meta?.createSignature,
|
||||||
docSignatures: meta?.docSignatures,
|
docSignatures: meta?.docSignatures,
|
||||||
keys: meta?.keys,
|
keys: meta?.keys,
|
||||||
|
timestamps: meta?.timestamps,
|
||||||
isValid,
|
isValid,
|
||||||
kind,
|
kind,
|
||||||
tags,
|
tags,
|
||||||
|
@ -40,7 +40,8 @@ import {
|
|||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
updateUsersAppData,
|
updateUsersAppData,
|
||||||
uploadToFileStorage,
|
uploadToFileStorage,
|
||||||
DEFAULT_TOOLBOX
|
DEFAULT_TOOLBOX,
|
||||||
|
settleAllFullfilfedPromises
|
||||||
} 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'
|
||||||
@ -61,7 +62,8 @@ import {
|
|||||||
faTrash,
|
faTrash,
|
||||||
faUpload
|
faUpload
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { SigitFile } from '../../utils/file.ts'
|
import { getSigitFile, SigitFile } from '../../utils/file.ts'
|
||||||
|
import _ from 'lodash'
|
||||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
|
|
||||||
export const CreatePage = () => {
|
export const CreatePage = () => {
|
||||||
@ -108,9 +110,31 @@ export const CreatePage = () => {
|
|||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
|
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
|
||||||
|
const [parsingPdf, setIsParsing] = useState<boolean>(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedFiles) {
|
||||||
|
/**
|
||||||
|
* Reads the binary files and converts to internal file type
|
||||||
|
* and sets to a state (adds images if it's a PDF)
|
||||||
|
*/
|
||||||
|
const parsePages = async () => {
|
||||||
|
const files = await settleAllFullfilfedPromises(
|
||||||
|
selectedFiles,
|
||||||
|
getSigitFile
|
||||||
|
)
|
||||||
|
|
||||||
|
setDrawnFiles(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsParsing(true)
|
||||||
|
|
||||||
|
parsePages().finally(() => {
|
||||||
|
setIsParsing(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [selectedFiles])
|
||||||
|
|
||||||
const [selectedTool, setSelectedTool] = useState<DrawTool>()
|
const [selectedTool, setSelectedTool] = useState<DrawTool>()
|
||||||
const [toolbox] = useState<DrawTool[]>(DEFAULT_TOOLBOX)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the drawing tool
|
* Changes the drawing tool
|
||||||
@ -284,6 +308,19 @@ export const CreatePage = () => {
|
|||||||
|
|
||||||
const handleRemoveUser = (pubkey: string) => {
|
const handleRemoveUser = (pubkey: string) => {
|
||||||
setUsers((prev) => prev.filter((user) => user.pubkey !== pubkey))
|
setUsers((prev) => prev.filter((user) => user.pubkey !== pubkey))
|
||||||
|
|
||||||
|
// Set counterpart to ''
|
||||||
|
const drawnFilesCopy = _.cloneDeep(drawnFiles)
|
||||||
|
drawnFilesCopy.forEach((s) => {
|
||||||
|
s.pages?.forEach((p) => {
|
||||||
|
p.drawnFields.forEach((d) => {
|
||||||
|
if (d.counterpart === hexToNpub(pubkey)) {
|
||||||
|
d.counterpart = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
setDrawnFiles(drawnFilesCopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -654,6 +691,8 @@ export const CreatePage = () => {
|
|||||||
const keys = await generateKeys(pubkeys, encryptionKey)
|
const keys = await generateKeys(pubkeys, encryptionKey)
|
||||||
if (!keys) return
|
if (!keys) return
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||||
|
|
||||||
const timestamp = await generateTimestamp(
|
const timestamp = await generateTimestamp(
|
||||||
extractNostrId(createSignature)
|
extractNostrId(createSignature)
|
||||||
)
|
)
|
||||||
@ -739,10 +778,6 @@ export const CreatePage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDrawFieldsChange = (sigitFiles: SigitFile[]) => {
|
|
||||||
setDrawnFiles(sigitFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authUrl) {
|
if (authUrl) {
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
@ -856,20 +891,27 @@ export const CreatePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`${styles.paperGroup} ${styles.toolbox}`}>
|
<div className={`${styles.paperGroup} ${styles.toolbox}`}>
|
||||||
{toolbox.map((drawTool: DrawTool, index: number) => {
|
{DEFAULT_TOOLBOX.filter((drawTool) => !drawTool.isHidden).map(
|
||||||
|
(drawTool: DrawTool, index: number) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
{...(drawTool.active && {
|
{...(!drawTool.isComingSoon && {
|
||||||
onClick: () => handleToolSelect(drawTool)
|
onClick: () => handleToolSelect(drawTool)
|
||||||
})}
|
})}
|
||||||
className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${!drawTool.active ? styles.comingSoon : ''}
|
className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${drawTool.isComingSoon ? styles.comingSoon : ''}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon fontSize={'15px'} icon={drawTool.icon} />
|
<FontAwesomeIcon
|
||||||
|
fontSize={'15px'}
|
||||||
|
icon={drawTool.icon}
|
||||||
|
/>
|
||||||
{drawTool.label}
|
{drawTool.label}
|
||||||
{drawTool.active ? (
|
{!drawTool.isComingSoon ? (
|
||||||
<FontAwesomeIcon fontSize={'15px'} icon={faEllipsis} />
|
<FontAwesomeIcon
|
||||||
|
fontSize={'15px'}
|
||||||
|
icon={faEllipsis}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className={styles.comingSoonPlaceholder}>
|
<span className={styles.comingSoonPlaceholder}>
|
||||||
Coming soon
|
Coming soon
|
||||||
@ -877,7 +919,8 @@ export const CreatePage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
}
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleCreate} variant="contained">
|
<Button onClick={handleCreate} variant="contained">
|
||||||
@ -893,13 +936,17 @@ export const CreatePage = () => {
|
|||||||
centerIcon={faFile}
|
centerIcon={faFile}
|
||||||
rightIcon={faToolbox}
|
rightIcon={faToolbox}
|
||||||
>
|
>
|
||||||
|
{parsingPdf ? (
|
||||||
|
<LoadingSpinner variant="small" />
|
||||||
|
) : (
|
||||||
<DrawPDFFields
|
<DrawPDFFields
|
||||||
metadata={metadata}
|
|
||||||
users={users}
|
users={users}
|
||||||
selectedFiles={selectedFiles}
|
metadata={metadata}
|
||||||
onDrawFieldsChange={onDrawFieldsChange}
|
|
||||||
selectedTool={selectedTool}
|
selectedTool={selectedTool}
|
||||||
|
sigitFiles={drawnFiles}
|
||||||
|
setSigitFiles={setDrawnFiles}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</StickySideColumns>
|
</StickySideColumns>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
import { Box, IconButton, SxProps, Theme, Typography } from '@mui/material'
|
import { Box, IconButton, SxProps, Theme, Typography } from '@mui/material'
|
||||||
import { truncate } from 'lodash'
|
|
||||||
import { Event, VerifiedEvent, kinds, nip19 } from 'nostr-tools'
|
import { Event, VerifiedEvent, kinds, nip19 } from 'nostr-tools'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
@ -14,6 +13,7 @@ import { State } from '../../store/rootReducer'
|
|||||||
import { NostrJoiningBlock, ProfileMetadata } from '../../types'
|
import { NostrJoiningBlock, ProfileMetadata } from '../../types'
|
||||||
import {
|
import {
|
||||||
getNostrJoiningBlockNumber,
|
getNostrJoiningBlockNumber,
|
||||||
|
getProfileUsername,
|
||||||
getRoboHashPicture,
|
getRoboHashPicture,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
shorten
|
shorten
|
||||||
@ -42,15 +42,7 @@ export const ProfilePage = () => {
|
|||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [loadingSpinnerDesc] = useState('Fetching metadata')
|
const [loadingSpinnerDesc] = useState('Fetching metadata')
|
||||||
|
|
||||||
const profileName =
|
const profileName = pubkey && getProfileUsername(pubkey, profileMetadata)
|
||||||
pubkey &&
|
|
||||||
profileMetadata &&
|
|
||||||
truncate(
|
|
||||||
profileMetadata.display_name || profileMetadata.name || hexToNpub(pubkey),
|
|
||||||
{
|
|
||||||
length: 16
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (npub) {
|
if (npub) {
|
||||||
|
@ -550,6 +550,8 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
const updatedMeta = updateMetaSignatures(meta, signedEvent)
|
const updatedMeta = updateMetaSignatures(meta, signedEvent)
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||||
|
|
||||||
const timestamp = await generateTimestamp(signedEvent.id)
|
const timestamp = await generateTimestamp(signedEvent.id)
|
||||||
if (timestamp) {
|
if (timestamp) {
|
||||||
updatedMeta.timestamps = [...(updatedMeta.timestamps || []), timestamp]
|
updatedMeta.timestamps = [...(updatedMeta.timestamps || []), timestamp]
|
||||||
|
@ -5,7 +5,13 @@ 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 {
|
||||||
|
DocSignatureEvent,
|
||||||
|
Meta,
|
||||||
|
SignedEvent,
|
||||||
|
OpenTimestamp,
|
||||||
|
OpenTimestampUpgradeVerifyResponse
|
||||||
|
} from '../../types'
|
||||||
import {
|
import {
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
extractMarksFromSignedMeta,
|
extractMarksFromSignedMeta,
|
||||||
@ -15,7 +21,10 @@ import {
|
|||||||
parseJson,
|
parseJson,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
getCurrentUserFiles
|
getCurrentUserFiles,
|
||||||
|
updateUsersAppData,
|
||||||
|
npubToHex,
|
||||||
|
sendNotification
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
@ -34,7 +43,7 @@ import { saveAs } from 'file-saver'
|
|||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
||||||
import { 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'
|
||||||
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'
|
||||||
@ -48,6 +57,8 @@ import {
|
|||||||
faFile,
|
faFile,
|
||||||
faFileDownload
|
faFileDownload
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
interface PdfViewProps {
|
interface PdfViewProps {
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
@ -180,7 +191,8 @@ export const VerifyPage = () => {
|
|||||||
signers,
|
signers,
|
||||||
viewers,
|
viewers,
|
||||||
fileHashes,
|
fileHashes,
|
||||||
parsedSignatureEvents
|
parsedSignatureEvents,
|
||||||
|
timestamps
|
||||||
} = useSigitMeta(meta)
|
} = useSigitMeta(meta)
|
||||||
|
|
||||||
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
||||||
@ -190,6 +202,16 @@ export const VerifyPage = () => {
|
|||||||
[key: string]: string | null
|
[key: string]: string | null
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
|
const signTimestampEvent = async (signerContent: {
|
||||||
|
timestamps: OpenTimestamp[]
|
||||||
|
}): Promise<SignedEvent | null> => {
|
||||||
|
return await signEventForMetaFile(
|
||||||
|
JSON.stringify(signerContent),
|
||||||
|
nostrController,
|
||||||
|
setIsLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Object.entries(files).length > 0) {
|
if (Object.entries(files).length > 0) {
|
||||||
const tmp = getCurrentUserFiles(files, currentFileHashes, fileHashes)
|
const tmp = getCurrentUserFiles(files, currentFileHashes, fileHashes)
|
||||||
@ -198,6 +220,115 @@ export const VerifyPage = () => {
|
|||||||
}, [currentFileHashes, fileHashes, files])
|
}, [currentFileHashes, fileHashes, files])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('timestamps: ', timestamps)
|
||||||
|
if (timestamps && timestamps.length > 0) {
|
||||||
|
console.log(timestamps.every((t) => !!t.verification))
|
||||||
|
if (timestamps.every((t) => !!t.verification)) {
|
||||||
|
toast.success('All of your timestamps are fully verified on Bitcoin.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const upgradeT = async (timestamps: OpenTimestamp[]) => {
|
||||||
|
try {
|
||||||
|
setLoadingSpinnerDesc('Upgrading and verifying your timestamps.')
|
||||||
|
const verifiedResults = await Promise.all(
|
||||||
|
timestamps.map(upgradeAndVerifyTimestamp)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if timestamp verification has been achieved for the first time.
|
||||||
|
* Note that the upgrade flag is separate from verification. It is possible for a timestamp
|
||||||
|
* to not be upgraded, but to be verified for the first time.
|
||||||
|
* @param upgradedTimestamp
|
||||||
|
* @param timestamps
|
||||||
|
*/
|
||||||
|
const isNewlyVerified = (
|
||||||
|
upgradedTimestamp: OpenTimestampUpgradeVerifyResponse,
|
||||||
|
timestamps: OpenTimestamp[]
|
||||||
|
) => {
|
||||||
|
if (!upgradedTimestamp.verified) return false
|
||||||
|
const oldT = timestamps.find(
|
||||||
|
(t) => t.nostrId === upgradedTimestamp.timestamp.nostrId
|
||||||
|
)
|
||||||
|
if (!oldT) return false
|
||||||
|
if (!oldT.verification && upgradedTimestamp.verified) return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const upgradedTimestamps = verifiedResults
|
||||||
|
.filter((t) => t.upgraded || isNewlyVerified(t, timestamps))
|
||||||
|
.map((t) => {
|
||||||
|
const timestamp = t.timestamp
|
||||||
|
if (t.verified) {
|
||||||
|
timestamp.verification = t.verification
|
||||||
|
}
|
||||||
|
return timestamp
|
||||||
|
})
|
||||||
|
|
||||||
|
if (upgradedTimestamps.length === 0) {
|
||||||
|
toast.success('No timestamp upgrades found.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Signing a timestamp upgrade event.')
|
||||||
|
|
||||||
|
const signedEvent = await signTimestampEvent({
|
||||||
|
timestamps: upgradedTimestamps
|
||||||
|
})
|
||||||
|
if (!signedEvent) return
|
||||||
|
|
||||||
|
const finalTimestamps = timestamps.map((t) => {
|
||||||
|
const upgraded = upgradedTimestamps.find(
|
||||||
|
(tu) => tu.nostrId === t.nostrId
|
||||||
|
)
|
||||||
|
if (upgraded) {
|
||||||
|
return {
|
||||||
|
...upgraded,
|
||||||
|
signature: JSON.stringify(signedEvent, null, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedMeta = _.cloneDeep(meta)
|
||||||
|
updatedMeta.timestamps = [...finalTimestamps]
|
||||||
|
updatedMeta.modifiedAt = unixNow()
|
||||||
|
|
||||||
|
const updatedEvent = await updateUsersAppData(updatedMeta)
|
||||||
|
if (!updatedEvent) return
|
||||||
|
|
||||||
|
const userSet = new Set<`npub1${string}`>()
|
||||||
|
signers.forEach((signer) => {
|
||||||
|
if (signer !== usersPubkey) {
|
||||||
|
userSet.add(signer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
viewers.forEach((viewer) => {
|
||||||
|
userSet.add(viewer)
|
||||||
|
})
|
||||||
|
|
||||||
|
const users = Array.from(userSet)
|
||||||
|
const promises = users.map((user) =>
|
||||||
|
sendNotification(npubToHex(user)!, updatedMeta)
|
||||||
|
)
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
|
toast.success('Timestamp updates have been sent successfully.')
|
||||||
|
|
||||||
|
setMeta(meta)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toast.error(
|
||||||
|
'There was an error upgrading or verifying your timestamps!'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
upgradeT(timestamps)
|
||||||
|
}
|
||||||
|
}, [timestamps])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('this runs')
|
||||||
if (metaInNavState && encryptionKey) {
|
if (metaInNavState && encryptionKey) {
|
||||||
const processSigit = async () => {
|
const processSigit = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
@ -18,7 +18,7 @@ export interface Meta {
|
|||||||
docSignatures: { [key: `npub1${string}`]: string }
|
docSignatures: { [key: `npub1${string}`]: string }
|
||||||
exportSignature?: string
|
exportSignature?: string
|
||||||
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
|
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
|
||||||
timestamps?: Timestamp[]
|
timestamps?: OpenTimestamp[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateSignatureEventContent {
|
export interface CreateSignatureEventContent {
|
||||||
@ -40,9 +40,23 @@ export interface Sigit {
|
|||||||
meta: Meta
|
meta: Meta
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Timestamp {
|
export interface OpenTimestamp {
|
||||||
nostrId: string
|
nostrId: string
|
||||||
timestamp: string
|
value: string
|
||||||
|
verification?: OpenTimestampVerification
|
||||||
|
signature?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenTimestampVerification {
|
||||||
|
height: number
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenTimestampUpgradeVerifyResponse {
|
||||||
|
timestamp: OpenTimestamp
|
||||||
|
upgraded: boolean
|
||||||
|
verified?: boolean
|
||||||
|
verification?: OpenTimestampVerification
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserAppData {
|
export interface UserAppData {
|
||||||
|
@ -31,7 +31,10 @@ export interface DrawTool {
|
|||||||
icon: IconDefinition
|
icon: IconDefinition
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
active?: boolean
|
/** show or hide the toolbox item */
|
||||||
|
isHidden?: boolean
|
||||||
|
/** show or hide "coming soon" message on the toolbox item */
|
||||||
|
isComingSoon?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MarkType {
|
export enum MarkType {
|
||||||
|
12
src/types/opentimestamps.d.ts
vendored
12
src/types/opentimestamps.d.ts
vendored
@ -3,13 +3,17 @@ interface OpenTimestamps {
|
|||||||
DetachedTimestampFile: {
|
DetachedTimestampFile: {
|
||||||
fromHash(op: any, hash: Uint8Array): any
|
fromHash(op: any, hash: Uint8Array): any
|
||||||
fromBytes(op: any, buffer: Uint8Array): any
|
fromBytes(op: any, buffer: Uint8Array): any
|
||||||
|
deserialize(buffer: any): any
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stamp the provided timestamp file and return a Promise
|
// Stamp the provided timestamp file and return a Promise
|
||||||
stamp(file: any): Promise<void>
|
stamp(file: any): Promise<void>
|
||||||
|
|
||||||
// Verify the provided timestamp proof file
|
// Verify the provided timestamp proof file
|
||||||
verify(file: any): Promise<void>
|
verify(
|
||||||
|
ots: string,
|
||||||
|
file: string
|
||||||
|
): Promise<TimestampVerficiationResponse | Record<string, never>>
|
||||||
|
|
||||||
// Other utilities or operations (like OpSHA256, serialization)
|
// Other utilities or operations (like OpSHA256, serialization)
|
||||||
Ops: {
|
Ops: {
|
||||||
@ -25,5 +29,9 @@ interface OpenTimestamps {
|
|||||||
deserialize(bytes: Uint8Array): any
|
deserialize(bytes: Uint8Array): any
|
||||||
|
|
||||||
// Other potential methods based on repo functions
|
// Other potential methods based on repo functions
|
||||||
upgrade(file: any): Promise<void>
|
upgrade(file: any): Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimestampVerficiationResponse {
|
||||||
|
bitcoin: { timestamp: number; height: number }
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export interface ProfileMetadata {
|
export interface ProfileMetadata {
|
||||||
name?: string
|
name?: string
|
||||||
display_name?: string
|
display_name?: string
|
||||||
|
/** @deprecated use name instead */
|
||||||
username?: string
|
username?: string
|
||||||
picture?: string
|
picture?: string
|
||||||
banner?: string
|
banner?: string
|
||||||
|
@ -3,26 +3,26 @@ 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 { DrawTool, MarkType } from '../types/drawing.ts'
|
||||||
import {
|
import {
|
||||||
faT,
|
faT,
|
||||||
faSignature,
|
faSignature,
|
||||||
faBriefcase,
|
faBriefcase,
|
||||||
faIdCard,
|
faIdCard,
|
||||||
faHeading,
|
|
||||||
faClock,
|
faClock,
|
||||||
faCalendarDays,
|
|
||||||
fa1,
|
fa1,
|
||||||
faImage,
|
faCalendarDays,
|
||||||
faSquareCheck,
|
|
||||||
faCheckDouble,
|
faCheckDouble,
|
||||||
faPaperclip,
|
|
||||||
faCircleDot,
|
faCircleDot,
|
||||||
faSquareCaretDown,
|
|
||||||
faTableCellsLarge,
|
|
||||||
faStamp,
|
|
||||||
faCreditCard,
|
faCreditCard,
|
||||||
faPhone
|
faHeading,
|
||||||
|
faImage,
|
||||||
|
faPaperclip,
|
||||||
|
faPhone,
|
||||||
|
faSquareCaretDown,
|
||||||
|
faSquareCheck,
|
||||||
|
faStamp,
|
||||||
|
faTableCellsLarge
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,114 +152,113 @@ 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 = [
|
export const DEFAULT_TOOLBOX: DrawTool[] = [
|
||||||
{
|
{
|
||||||
identifier: MarkType.TEXT,
|
identifier: MarkType.FULLNAME,
|
||||||
icon: faT,
|
icon: faIdCard,
|
||||||
label: 'Text',
|
label: 'Full Name',
|
||||||
active: true
|
isComingSoon: true
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.SIGNATURE,
|
|
||||||
icon: faSignature,
|
|
||||||
label: 'Signature',
|
|
||||||
active: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.JOBTITLE,
|
identifier: MarkType.JOBTITLE,
|
||||||
icon: faBriefcase,
|
icon: faBriefcase,
|
||||||
label: 'Job Title',
|
label: 'Job Title',
|
||||||
active: false
|
isComingSoon: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.FULLNAME,
|
identifier: MarkType.SIGNATURE,
|
||||||
icon: faIdCard,
|
icon: faSignature,
|
||||||
label: 'Full Name',
|
label: 'Signature',
|
||||||
active: false
|
isComingSoon: true
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.INITIALS,
|
|
||||||
icon: faHeading,
|
|
||||||
label: 'Initials',
|
|
||||||
active: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.DATETIME,
|
identifier: MarkType.DATETIME,
|
||||||
icon: faClock,
|
icon: faClock,
|
||||||
label: 'Date Time',
|
label: 'Date Time',
|
||||||
active: false
|
isComingSoon: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.DATE,
|
identifier: MarkType.TEXT,
|
||||||
icon: faCalendarDays,
|
icon: faT,
|
||||||
label: 'Date',
|
label: 'Text'
|
||||||
active: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.NUMBER,
|
identifier: MarkType.NUMBER,
|
||||||
icon: fa1,
|
icon: fa1,
|
||||||
label: 'Number',
|
label: 'Number',
|
||||||
active: false
|
isComingSoon: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.INITIALS,
|
||||||
|
icon: faHeading,
|
||||||
|
label: 'Initials',
|
||||||
|
isHidden: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.DATE,
|
||||||
|
icon: faCalendarDays,
|
||||||
|
label: 'Date',
|
||||||
|
isHidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.IMAGES,
|
identifier: MarkType.IMAGES,
|
||||||
icon: faImage,
|
icon: faImage,
|
||||||
label: 'Images',
|
label: 'Images',
|
||||||
active: false
|
isHidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.CHECKBOX,
|
identifier: MarkType.CHECKBOX,
|
||||||
icon: faSquareCheck,
|
icon: faSquareCheck,
|
||||||
label: 'Checkbox',
|
label: 'Checkbox',
|
||||||
active: false
|
isHidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.MULTIPLE,
|
identifier: MarkType.MULTIPLE,
|
||||||
icon: faCheckDouble,
|
icon: faCheckDouble,
|
||||||
label: 'Multiple',
|
label: 'Multiple',
|
||||||
active: false
|
isHidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.FILE,
|
identifier: MarkType.FILE,
|
||||||
icon: faPaperclip,
|
icon: faPaperclip,
|
||||||
label: 'File',
|
label: 'File',
|
||||||
active: false
|
isHidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.RADIO,
|
identifier: MarkType.RADIO,
|
||||||
icon: faCircleDot,
|
icon: faCircleDot,
|
||||||
label: 'Radio',
|
label: 'Radio',
|
||||||
active: false
|
isHidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.SELECT,
|
identifier: MarkType.SELECT,
|
||||||
icon: faSquareCaretDown,
|
icon: faSquareCaretDown,
|
||||||
label: 'Select',
|
label: 'Select',
|
||||||
active: false
|
isHidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.CELLS,
|
identifier: MarkType.CELLS,
|
||||||
icon: faTableCellsLarge,
|
icon: faTableCellsLarge,
|
||||||
label: 'Cells',
|
label: 'Cells',
|
||||||
active: false
|
isHidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.STAMP,
|
identifier: MarkType.STAMP,
|
||||||
icon: faStamp,
|
icon: faStamp,
|
||||||
label: 'Stamp',
|
label: 'Stamp',
|
||||||
active: false
|
isHidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.PAYMENT,
|
identifier: MarkType.PAYMENT,
|
||||||
icon: faCreditCard,
|
icon: faCreditCard,
|
||||||
label: 'Payment',
|
label: 'Payment',
|
||||||
active: false
|
isHidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: MarkType.PHONE,
|
identifier: MarkType.PHONE,
|
||||||
icon: faPhone,
|
icon: faPhone,
|
||||||
label: 'Phone',
|
label: 'Phone',
|
||||||
active: false
|
isHidden: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import _, { truncate } from 'lodash'
|
||||||
import {
|
import {
|
||||||
Event,
|
Event,
|
||||||
EventTemplate,
|
EventTemplate,
|
||||||
@ -30,7 +30,7 @@ import {
|
|||||||
import { AuthState, Keys } from '../store/auth/types'
|
import { AuthState, Keys } from '../store/auth/types'
|
||||||
import { RelaysState } from '../store/relays/types'
|
import { RelaysState } from '../store/relays/types'
|
||||||
import store from '../store/store'
|
import store from '../store/store'
|
||||||
import { Meta, SignedEvent, UserAppData } from '../types'
|
import { Meta, ProfileMetadata, SignedEvent, UserAppData } from '../types'
|
||||||
import { getDefaultRelayMap } from './relays'
|
import { getDefaultRelayMap } from './relays'
|
||||||
import { parseJson, removeLeadingSlash } from './string'
|
import { parseJson, removeLeadingSlash } from './string'
|
||||||
import { timeout } from './utils'
|
import { timeout } from './utils'
|
||||||
@ -974,3 +974,16 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
|
|||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show user's name, first available in order: display_name, name, or npub as fallback
|
||||||
|
* @param npub User identifier, it can be either pubkey or npub1 (we only show npub)
|
||||||
|
* @param profile User profile
|
||||||
|
*/
|
||||||
|
export const getProfileUsername = (
|
||||||
|
npub: `npub1${string}` | string,
|
||||||
|
profile?: ProfileMetadata
|
||||||
|
) =>
|
||||||
|
truncate(profile?.display_name || profile?.name || hexToNpub(npub), {
|
||||||
|
length: 16
|
||||||
|
})
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Timestamp } from '../types'
|
import { OpenTimestamp, OpenTimestampUpgradeVerifyResponse } from '../types'
|
||||||
import { retry } from './retry.ts'
|
import { retry } from './retry.ts'
|
||||||
import { bytesToHex } from '@noble/hashes/utils'
|
import { bytesToHex } from '@noble/hashes/utils'
|
||||||
import { utf8Encoder } from 'nostr-tools/utils'
|
import { utf8Encoder } from 'nostr-tools/utils'
|
||||||
|
import { hexStringToUint8Array } from './string.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a timestamp for the provided nostr event ID.
|
* Generates a timestamp for the provided nostr event ID.
|
||||||
@ -9,10 +10,10 @@ import { utf8Encoder } from 'nostr-tools/utils'
|
|||||||
*/
|
*/
|
||||||
export const generateTimestamp = async (
|
export const generateTimestamp = async (
|
||||||
nostrId: string
|
nostrId: string
|
||||||
): Promise<Timestamp | undefined> => {
|
): Promise<OpenTimestamp | undefined> => {
|
||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
timestamp: await retry(() => timestamp(nostrId)),
|
value: await retry(() => timestamp(nostrId)),
|
||||||
nostrId: nostrId
|
nostrId: nostrId
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -21,6 +22,89 @@ export const generateTimestamp = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to upgrade (i.e. add Bitcoin blockchain attestations) and verify the provided timestamp.
|
||||||
|
* Returns the same timestamp, alongside additional information required to decide if any further
|
||||||
|
* timestamp updates are required.
|
||||||
|
* @param timestamp
|
||||||
|
*/
|
||||||
|
export const upgradeAndVerifyTimestamp = async (
|
||||||
|
timestamp: OpenTimestamp
|
||||||
|
): Promise<OpenTimestampUpgradeVerifyResponse> => {
|
||||||
|
const upgradedResult = await upgrade(timestamp)
|
||||||
|
return await verify(upgradedResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to upgrade a timestamp. If an upgrade is available,
|
||||||
|
* it will add new data to detachedTimestamp.
|
||||||
|
* The upgraded flag indicates if an upgrade has been performed.
|
||||||
|
* @param t - timestamp
|
||||||
|
*/
|
||||||
|
const upgrade = async (
|
||||||
|
t: OpenTimestamp
|
||||||
|
): Promise<OpenTimestampUpgradeVerifyResponse> => {
|
||||||
|
console.log('timestamp: ', t)
|
||||||
|
const detachedTimestamp =
|
||||||
|
window.OpenTimestamps.DetachedTimestampFile.deserialize(
|
||||||
|
hexStringToUint8Array(t.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const changed: boolean =
|
||||||
|
await window.OpenTimestamps.upgrade(detachedTimestamp)
|
||||||
|
if (changed) {
|
||||||
|
const updated = detachedTimestamp.serializeToBytes()
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
...t,
|
||||||
|
timestamp: bytesToHex(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: value,
|
||||||
|
upgraded: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
timestamp: t,
|
||||||
|
upgraded: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to verify a timestamp. If verification is available,
|
||||||
|
* it will be included in the returned object.
|
||||||
|
* @param t - timestamp
|
||||||
|
*/
|
||||||
|
export const verify = async (
|
||||||
|
t: OpenTimestampUpgradeVerifyResponse
|
||||||
|
): Promise<OpenTimestampUpgradeVerifyResponse> => {
|
||||||
|
const detachedNostrId = window.OpenTimestamps.DetachedTimestampFile.fromBytes(
|
||||||
|
new window.OpenTimestamps.Ops.OpSHA256(),
|
||||||
|
utf8Encoder.encode(t.timestamp.nostrId)
|
||||||
|
)
|
||||||
|
|
||||||
|
const detachedTimestamp =
|
||||||
|
window.OpenTimestamps.DetachedTimestampFile.deserialize(
|
||||||
|
hexStringToUint8Array(t.timestamp.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await window.OpenTimestamps.verify(
|
||||||
|
detachedTimestamp,
|
||||||
|
detachedNostrId
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...t,
|
||||||
|
verified: !!res.bitcoin,
|
||||||
|
verification: res?.bitcoin || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps a nostrId.
|
||||||
|
* @param nostrId
|
||||||
|
*/
|
||||||
const timestamp = async (nostrId: string): Promise<string> => {
|
const timestamp = async (nostrId: string): Promise<string> => {
|
||||||
const detachedTimestamp =
|
const detachedTimestamp =
|
||||||
window.OpenTimestamps.DetachedTimestampFile.fromBytes(
|
window.OpenTimestamps.DetachedTimestampFile.fromBytes(
|
||||||
|
Loading…
Reference in New Issue
Block a user