Compare commits

..

10 Commits

Author SHA1 Message Date
6ba3b6ec89 Merge branch 'staging' into issue-171
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-16 12:37:39 +00:00
b
aa8214d015 Merge branch 'staging' into issue-171
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-13 10:07:24 +00:00
e48a396990 fix: verify/sign link
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-11 17:29:47 +02:00
79e14d45a1 chore: comment fix
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-11 15:41:42 +02:00
64e8ebba85 chore: renamed sigitKey to sigitCreateId
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-11 13:30:39 +02:00
5dc8d53503 chore: sigitCreateId naming
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-09-11 12:33:40 +02:00
86a16c13ce chore: comments and lint (typing)
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-09-11 12:29:38 +02:00
7c027825cd style: lint fix
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-11 12:03:23 +02:00
8e71592d88 fix: routing, removed useEffect 2024-09-11 11:59:12 +02:00
75a715d002 feat: Add Sigit ID as a path param
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-09-10 16:00:48 +02:00
26 changed files with 379 additions and 470 deletions

10
package-lock.json generated
View File

@ -21,7 +21,6 @@
"@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "2.5.0",
"@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",

View File

@ -31,7 +31,6 @@
"@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "2.5.0",
"@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4",
"crypto-hash": "3.0.0",

View File

@ -100,10 +100,10 @@ li {
// - first-child Header height, default body padding, and center content border (10px) and padding (10px)
// - others We don't include border and padding and scroll to the top of the image
&:first-child {
scroll-margin-top: $body-vertical-padding + 20px;
scroll-margin-top: $header-height + $body-vertical-padding + 20px;
}
&:not(:first-child) {
scroll-margin-top: $body-vertical-padding;
scroll-margin-top: $header-height + $body-vertical-padding;
}
}
@ -135,20 +135,14 @@ li {
// Consistent styling for every file mark
// Reverts some of the design defaults for font
.file-mark {
font-family: 'Roboto';
font-style: normal;
font-weight: normal;
letter-spacing: normal;
line-height: 1;
font-family: Arial;
font-size: 16px;
font-weight: normal;
color: black;
outline: 1px solid transparent;
letter-spacing: normal;
border: 1px solid transparent;
justify-content: start;
align-items: start;
scroll-margin-top: $body-vertical-padding;
scroll-margin-top: $header-height + $body-vertical-padding;
}
[data-dev='true'] {

Binary file not shown.

View File

@ -36,9 +36,6 @@ import styles from './style.module.scss'
import { setUserRobotImage } from '../../store/userRobotImage/action'
import { Container } from '../Container'
import { ButtonIcon } from '../ButtonIcon'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faClose } from '@fortawesome/free-solid-svg-icons'
import useMediaQuery from '@mui/material/useMediaQuery'
const metadataController = MetadataController.getInstance()
@ -121,150 +118,125 @@ export const AppBar = () => {
}
const isAuthenticated = authState?.loggedIn === true
const matches = useMediaQuery('(max-width:767px)')
const [isBannerVisible, setIsBannerVisible] = useState(true)
const handleBannerHide = () => {
setIsBannerVisible(false)
}
return (
<>
{isAuthenticated && isBannerVisible && (
<div className={styles.banner}>
<Container>
<div className={styles.bannerInner}>
SIGit is currently Alpha software (available for internal
testing), use at your own risk!
<AppBarMui
position="fixed"
className={styles.AppBar}
sx={{
boxShadow: 'none'
}}
>
<Container>
<Toolbar className={styles.toolbar} disableGutters={true}>
<Box className={styles.logoWrapper}>
<img src="/logo.svg" alt="Logo" onClick={() => navigate('/')} />
</Box>
<Box className={styles.rightSideBox}>
{!isAuthenticated && (
<Button
aria-label={`close banner`}
variant="text"
onClick={handleBannerHide}
startIcon={<ButtonIcon />}
onClick={() => {
navigate(appPublicRoutes.nostr)
}}
variant="contained"
>
<FontAwesomeIcon icon={faClose} />
LOGIN
</Button>
</div>
</Container>
</div>
)}
<AppBarMui
position={matches ? 'sticky' : 'static'}
className={styles.AppBar}
sx={{
boxShadow: 'none'
}}
>
<Container>
<Toolbar className={styles.toolbar} disableGutters={true}>
<Box className={styles.logoWrapper}>
<img src="/logo.svg" alt="Logo" onClick={() => navigate('/')} />
</Box>
)}
<Box className={styles.rightSideBox}>
{!isAuthenticated && (
<Button
startIcon={<ButtonIcon />}
onClick={() => {
navigate(appPublicRoutes.nostr)
{isAuthenticated && (
<>
<Username
username={username}
avatarContent={userAvatar}
handleClick={handleOpenUserMenu}
/>
<Menu
id="menu-appbar"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
variant="contained"
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'center'
}}
open={!!anchorElUser}
onClose={handleCloseUserMenu}
>
LOGIN
</Button>
)}
<MenuItem
sx={{
justifyContent: 'center',
display: { md: 'none' },
fontWeight: 500,
fontSize: '14px',
color: 'var(--text-color)'
}}
>
<Typography variant="h6">{username}</Typography>
</MenuItem>
<MenuItem
onClick={handleProfile}
sx={{
justifyContent: 'center'
}}
>
Profile
</MenuItem>
<MenuItem
onClick={() => {
setAnchorElUser(null)
{isAuthenticated && (
<>
<Username
username={username}
avatarContent={userAvatar}
handleClick={handleOpenUserMenu}
/>
<Menu
id="menu-appbar"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
navigate(appPrivateRoutes.settings)
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'center'
sx={{
justifyContent: 'center'
}}
open={!!anchorElUser}
onClose={handleCloseUserMenu}
>
Settings
</MenuItem>
<MenuItem
onClick={() => {
setAnchorElUser(null)
navigate(appPublicRoutes.verify)
}}
sx={{
justifyContent: 'center'
}}
>
Verify
</MenuItem>
<Link
to={appPublicRoutes.source}
target="_blank"
style={{ color: 'inherit', textDecoration: 'inherit' }}
>
<MenuItem
sx={{
justifyContent: 'center',
display: { md: 'none' },
fontWeight: 500,
fontSize: '14px',
color: 'var(--text-color)'
}}
>
<Typography variant="h6">{username}</Typography>
</MenuItem>
<MenuItem
onClick={handleProfile}
sx={{
justifyContent: 'center'
}}
>
Profile
Source
</MenuItem>
<MenuItem
onClick={() => {
setAnchorElUser(null)
navigate(appPrivateRoutes.settings)
}}
sx={{
justifyContent: 'center'
}}
>
Settings
</MenuItem>
<MenuItem
onClick={() => {
setAnchorElUser(null)
navigate(appPublicRoutes.verify)
}}
sx={{
justifyContent: 'center'
}}
>
Verify
</MenuItem>
<Link
to={appPublicRoutes.source}
target="_blank"
style={{ color: 'inherit', textDecoration: 'inherit' }}
>
<MenuItem
sx={{
justifyContent: 'center'
}}
>
Source
</MenuItem>
</Link>
<MenuItem
onClick={handleLogout}
sx={{
justifyContent: 'center'
}}
>
Logout
</MenuItem>
</Menu>
</>
)}
</Box>
</Toolbar>
</Container>
</AppBarMui>
</>
</Link>
<MenuItem
onClick={handleLogout}
sx={{
justifyContent: 'center'
}}
>
Logout
</MenuItem>
</Menu>
</>
)}
</Box>
</Toolbar>
</Container>
</AppBarMui>
)
}

View File

@ -34,36 +34,3 @@
justify-content: flex-end;
}
}
.banner {
color: #ffffff;
background-color: $primary-main;
}
.bannerInner {
display: flex;
gap: 10px;
padding-block: 10px;
z-index: 1;
width: 100%;
justify-content: space-between;
flex-direction: row;
button {
min-width: 44px;
color: inherit;
}
&:hover,
&.active,
&:focus-within {
background: $primary-main;
color: inherit;
button {
color: inherit;
}
}
}

View File

@ -22,11 +22,16 @@ import { useSigitMeta } from '../../hooks/useSigitMeta'
import { extractFileExtensions } from '../../utils/file'
type SigitProps = {
sigitCreateId: string
meta: Meta
parsedMeta: SigitCardDisplayInfo
}
export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
export const DisplaySigit = ({
meta,
parsedMeta,
sigitCreateId: sigitCreateId
}: SigitProps) => {
const { title, createdAt, submittedBy, signers, signedStatus, isValid } =
parsedMeta
@ -35,15 +40,19 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
return (
<div className={styles.itemWrapper}>
<Link
to={
signedStatus === SigitStatus.Complete
? appPublicRoutes.verify
: appPrivateRoutes.sign
}
state={{ meta }}
className={styles.insetLink}
></Link>
{signedStatus === SigitStatus.Complete && (
<Link
to={appPublicRoutes.verify}
state={{ meta }}
className={styles.insetLink}
></Link>
)}
{signedStatus !== SigitStatus.Complete && (
<Link
to={`${appPrivateRoutes.sign}/${sigitCreateId}`}
className={styles.insetLink}
></Link>
)}
<p className={`line-clamp-2 ${styles.title}`}>{title}</p>
<div className={styles.users}>
{submittedBy && (

View File

@ -14,10 +14,9 @@ import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
import { truncate } from 'lodash'
import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils'
import { getSigitFile, SigitFile } from '../../utils/file'
import { getToolboxLabelByMarkType } from '../../utils/mark'
import { FileDivider } from '../FileDivider'
import { ExtensionFileBox } from '../ExtensionFileBox'
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf'
import { inPx } from '../../utils/pdf'
import { useScale } from '../../hooks/useScale'
import { AvatarIconButton } from '../UserAvatarIconButton'
import { LoadingSpinner } from '../LoadingSpinner'
@ -391,7 +390,7 @@ export const DrawPDFFields = (props: Props) => {
backgroundColor: drawnField.counterpart
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}4b`
: undefined,
outlineColor: drawnField.counterpart
borderColor: drawnField.counterpart
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}`
: undefined,
left: inPx(from(page.width, drawnField.left)),
@ -407,16 +406,6 @@ export const DrawPDFFields = (props: Props) => {
: undefined
}}
>
<div
className={`file-mark ${styles.placeholder}`}
style={{
fontFamily: FONT_TYPE,
fontSize: inPx(from(page.width, FONT_SIZE))
}}
>
{getToolboxLabelByMarkType(drawnField.type) ||
'placeholder'}
</div>
<span
onPointerDown={(event) =>
handleResizePointerDown(event, drawnFieldIndex)

View File

@ -13,15 +13,9 @@
}
}
.placeholder {
position: absolute;
opacity: 0.5;
inset: 0;
}
.drawingRectangle {
position: absolute;
outline: 1px solid #01aaad;
border: 1px solid #01aaad;
z-index: 50;
background-color: #01aaad4b;
cursor: pointer;
@ -35,7 +29,7 @@
}
&.edited {
outline: 1px dotted #01aaad;
border: 1px dotted #01aaad;
}
.resizeHandle {

View File

@ -36,7 +36,7 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
backgroundColor: selectedMark?.mark.npub
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}4b`
: undefined,
outlineColor: selectedMark?.mark.npub
borderColor: selectedMark?.mark.npub
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}`
: undefined,
left: inPx(from(pageWidth, location.left)),

View File

@ -33,8 +33,7 @@ const PdfPageItem = ({
useEffect(() => {
if (selectedMark !== null && !!markRefs.current[selectedMark.id]) {
markRefs.current[selectedMark.id]?.scrollIntoView({
behavior: 'smooth',
block: 'center'
behavior: 'smooth'
})
}
}, [selectedMark])

View File

@ -42,22 +42,15 @@
.sides {
@media only screen and (min-width: 768px) {
position: sticky;
top: $body-vertical-padding;
top: $header-height + $body-vertical-padding;
}
> :first-child {
// We want to keep header on smaller devices at all times
max-height: calc(
100dvh - $header-height - $body-vertical-padding * 2 - $tabs-height
);
@media only screen and (min-width: 768px) {
max-height: calc(100dvh - $body-vertical-padding * 2);
}
}
}
// Adjust the content scroll on smaller screens
// Make sure only the inner tab is scrolling
.scrollAdjust {
@media only screen and (max-width: 767px) {
max-height: calc(

View File

@ -3,5 +3,5 @@
.main {
flex-grow: 1;
padding: $body-vertical-padding 0 $body-vertical-padding 0;
padding: $header-height + $body-vertical-padding 0 $body-vertical-padding 0;
}

View File

@ -38,24 +38,41 @@ import {
sendNotification,
signEventForMetaFile,
updateUsersAppData,
uploadToFileStorage,
DEFAULT_TOOLBOX
uploadToFileStorage
} from '../../utils'
import { Container } from '../../components/Container'
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 { Mark } from '../../types/mark.ts'
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
fa1,
faBriefcase,
faCalendarDays,
faCheckDouble,
faCircleDot,
faClock,
faCreditCard,
faEllipsis,
faEye,
faFile,
faFileCirclePlus,
faGripLines,
faHeading,
faIdCard,
faImage,
faPaperclip,
faPen,
faPhone,
faPlus,
faSignature,
faSquareCaretDown,
faSquareCheck,
faStamp,
faT,
faTableCellsLarge,
faToolbox,
faTrash,
faUpload
@ -108,7 +125,116 @@ export const CreatePage = () => {
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
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
@ -301,15 +427,7 @@ export const CreatePage = () => {
const handleSelectFiles = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
// Get the uploaded files
const files = Array.from(event.target.files)
// Remove duplicates based on the file.name
setSelectedFiles((p) =>
[...p, ...files].filter(
(file, i, array) => i === array.findIndex((t) => t.name === file.name)
)
)
setSelectedFiles(Array.from(event.target.files))
}
}

View File

@ -257,6 +257,7 @@ export const HomePage = () => {
.map((key) => (
<DisplaySigit
key={`sigit-${key}`}
sigitCreateId={key}
parsedMeta={parsedSigits[key]}
meta={sigits[key]}
/>

View File

@ -233,7 +233,6 @@ export const RelaysPage = () => {
))}
</Box>
)}
<Footer />
</Container>
)
}
@ -429,6 +428,7 @@ const RelayItem = ({
)}
</List>
</Box>
<Footer />
</>
)
}

View File

@ -7,7 +7,7 @@ import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools'
import { useCallback, useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { NostrController } from '../../controllers'
@ -54,6 +54,7 @@ import {
SigitFile
} from '../../utils/file.ts'
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
import { useAppSelector } from '../../hooks/store.ts'
enum SignedStatus {
Fully_Signed,
User_Is_Next_Signer,
@ -63,17 +64,39 @@ enum SignedStatus {
export const SignPage = () => {
const navigate = useNavigate()
const location = useLocation()
const params = useParams()
const usersAppData = useAppSelector((state) => state.userAppData)
/**
* Received from `location.state`
*
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains keys.json
* arrayBuffer will be received in navigation from create page in offline mode
* meta will be received in navigation from create & home page in online mode
* arrayBuffer (decryptedArrayBuffer) will be received in navigation from create page in offline mode
* meta (metaInNavState) will be received in navigation from create & home page in online mode
*/
const {
meta: metaInNavState,
arrayBuffer: decryptedArrayBuffer,
uploadedZip
} = location.state || {}
let metaInNavState = location?.state?.meta || undefined
const { arrayBuffer: decryptedArrayBuffer, uploadedZip } = location.state || {
decryptedArrayBuffer: undefined,
uploadedZip: undefined
}
/**
* If userAppData (redux) is available, and we have the route param (sigit id)
* which is actually a `createEventId`, we will fetch a `sigit`
* based on the provided route ID and set fetched `sigit` to the `metaInNavState`
*/
if (usersAppData) {
const sigitCreateId = params.id
if (sigitCreateId) {
const sigit = usersAppData.sigits[sigitCreateId]
if (sigit) {
metaInNavState = sigit
}
}
}
const [displayInput, setDisplayInput] = useState(false)

View File

@ -22,6 +22,7 @@ import { useLocation } from 'react-router-dom'
import axios from 'axios'
import {
addMarks,
convertToPdfBlob,
FONT_SIZE,
FONT_TYPE,
groupMarksByFileNamePage,
@ -398,7 +399,8 @@ export const VerifyPage = () => {
for (const [fileName, file] of Object.entries(files)) {
if (file.isPdf) {
// 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)
} else {
zip.file(`files/${fileName}`, file)

View File

@ -61,6 +61,6 @@
[data-dev='true'] {
.mark {
outline: 1px dotted black;
border: 1px dotted black;
}
}

View File

@ -129,7 +129,7 @@ export const privateRoutes = [
element: <CreatePage />
},
{
path: appPrivateRoutes.sign,
path: `${appPrivateRoutes.sign}/:id?`,
element: <SignPage />
},
{

View File

@ -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_BLOSSOM = 'https://blossom.sigit.io'
export const DEFAULT_LOOK_UP_RELAY_LIST = [
SIGIT_RELAY,
'wss://user.kindpag.es',

View File

@ -4,6 +4,7 @@ import { MOST_COMMON_MEDIA_TYPES } from './const.ts'
import { extractMarksFromSignedMeta } from './mark.ts'
import {
addMarks,
convertToPdfBlob,
groupMarksByFileNamePage,
isPdf,
pdfToImages
@ -21,7 +22,8 @@ export const getZipWithFiles = async (
for (const [fileName, file] of Object.entries(files)) {
if (file.isPdf) {
// 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)
} else {
// Handle other files

View File

@ -3,27 +3,6 @@ import { hexToNpub } from './nostr.ts'
import { Meta, SignedEventContent } from '../types'
import { Event } from 'nostr-tools'
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.
@ -152,121 +131,6 @@ const findOtherUserMarks = (marks: Mark[], pubkey: string): Mark[] => {
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 {
getCurrentUserMarks,
filterMarksByPubkey,

View File

@ -16,8 +16,6 @@ import { CreateSignatureEventContent, Meta } from '../types'
import { hexToNpub, unixNow } from './nostr'
import { parseJson } from './string'
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.
@ -27,18 +25,12 @@ import { SIGIT_BLOSSOM } from './const.ts'
*/
export const uploadToFileStorage = async (file: File) => {
// 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 = {
kind: 24242,
content: 'Authorize Upload',
created_at: unixNow(),
tags: [
['t', 'upload'],
['x', hash],
['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
['name', file.name],
['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
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
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
const response = await axios.put(`${FILE_STORAGE_URL}/upload`, file, {
headers: {
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)), // Set authorization header
'Content-Type': 'application/sigit' // Set content type header

View File

@ -35,7 +35,6 @@ import { getDefaultRelayMap } from './relays'
import { parseJson, removeLeadingSlash } from './string'
import { timeout } from './utils'
import { getHash } from './hash'
import { SIGIT_BLOSSOM } from './const.ts'
/**
* Generates a `d` tag for userAppData
@ -724,11 +723,6 @@ const uploadUserAppDataToBlossom = async (
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
const event: EventTemplate = {
kind: 24242,
@ -736,7 +730,6 @@ const uploadUserAppDataToBlossom = async (
created_at: unixNow(),
tags: [
['t', 'upload'],
['x', hash],
['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
['name', file.name],
['size', String(file.size)]
@ -746,8 +739,11 @@ const uploadUserAppDataToBlossom = async (
// Finalize the event with the private key
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
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
const response = await axios.put(`${FILE_STORAGE_URL}/upload`, file, {
headers: {
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header
}

View File

@ -1,6 +1,7 @@
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 * as PDFJS from 'pdfjs-dist'
import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker'
if (!PDFJS.GlobalWorkerOptions.workerPort) {
@ -9,23 +10,18 @@ if (!PDFJS.GlobalWorkerOptions.workerPort) {
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
* correlate font size used at the time of filling in / drawing on the PDF
* 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
/**
* Current font type used when generating a PDF.
*/
export const FONT_TYPE: string = 'Roboto'
/**
* Current line height used when generating a PDF.
*/
export const FONT_LINE_HEIGHT: number = 1
export const FONT_TYPE: string = 'Arial'
/**
* 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
* 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 (
file: File,
marksPerPage: { [key: string]: Mark[] }
) => {
const p = await readPdf(file)
const pdf = await PDFDocument.load(p)
const robotoFont = await embedFont(pdf)
const pages = pdf.getPages()
const pdf = await PDFJS.getDocument(p).promise
const canvas = document.createElement('canvas')
for (let i = 0; i < pages.length; i++) {
if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
marksPerPage[i]?.forEach((mark) =>
drawMarkText(mark, pages[i], robotoFont)
)
const images: string[] = []
for (let i = 0; i < pdf.numPages; i++) {
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
* @param mark to be drawn
* @param ctx a Canvas representation of a specific PDF Page
* @deprecated use drawMarkText
*/
export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
const { location } = mark
@ -168,37 +191,27 @@ export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
}
/**
* Draws a Mark on a PDF Page
* @param mark to be drawn
* @param page PDF Page
* @param font embedded font
* Takes an array of encoded PDF pages and returns a blob that is a complete PDF file
* @param markedPdfPages
*/
export const drawMarkText = (mark: Mark, page: PDFPage, font: PDFFont) => {
const { location } = mark
const { height } = page.getSize()
export const convertToPdfBlob = async (
markedPdfPages: string[]
): Promise<Blob> => {
const pdfDoc = await PDFDocument.create()
// Convert the mark location origin (top, left) to PDF origin (bottom, left)
const x = location.left
const y = height - location.top
for (const page of markedPdfPages) {
const pngImage = await pdfDoc.embedPng(page)
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
// We start from the y (top location border) and we need to bump it down
// 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
})
const pdfBytes = await pdfDoc.save()
return new Blob([pdfBytes], { type: 'application/pdf' })
}
/**
@ -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
}