From 18270c5d8afc376a6cef3b7cc3ea66a272797637 Mon Sep 17 00:00:00 2001 From: daniyal Date: Thu, 22 Aug 2024 13:08:30 +0500 Subject: [PATCH 01/58] feat: use nip04 for encryption and decryption of userData to store on blossom server --- src/store/actionTypes.ts | 1 + src/store/userAppData/action.ts | 11 ++++- src/store/userAppData/reducer.ts | 6 +++ src/store/userAppData/types.ts | 6 +++ src/types/core.ts | 1 + src/utils/nostr.ts | 74 +++++++++++++++++++++++++------- 6 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/store/actionTypes.ts b/src/store/actionTypes.ts index f0e6698..ff33627 100644 --- a/src/store/actionTypes.ts +++ b/src/store/actionTypes.ts @@ -18,3 +18,4 @@ export const SET_RELAY_MAP_UPDATED = 'SET_RELAY_MAP_UPDATED' export const UPDATE_USER_APP_DATA = 'UPDATE_USER_APP_DATA' export const UPDATE_PROCESSED_GIFT_WRAPS = 'UPDATE_PROCESSED_GIFT_WRAPS' +export const SET_D_TAG_FOR_APP_DATA = 'SET_D_TAG_FOR_APP_DATA' diff --git a/src/store/userAppData/action.ts b/src/store/userAppData/action.ts index 23bf9a1..2164aaa 100644 --- a/src/store/userAppData/action.ts +++ b/src/store/userAppData/action.ts @@ -1,6 +1,10 @@ import { UserAppData } from '../../types' import * as ActionTypes from '../actionTypes' -import { UpdateProcessedGiftWraps, UpdateUserAppData } from './types' +import { + SetDTagForAppData, + UpdateProcessedGiftWraps, + UpdateUserAppData +} from './types' export const updateUserAppData = (payload: UserAppData): UpdateUserAppData => ({ type: ActionTypes.UPDATE_USER_APP_DATA, @@ -13,3 +17,8 @@ export const updateProcessedGiftWraps = ( type: ActionTypes.UPDATE_PROCESSED_GIFT_WRAPS, payload }) + +export const setDTagForAppData = (payload: string): SetDTagForAppData => ({ + type: ActionTypes.SET_D_TAG_FOR_APP_DATA, + payload +}) diff --git a/src/store/userAppData/reducer.ts b/src/store/userAppData/reducer.ts index 22c7cb0..dacb375 100644 --- a/src/store/userAppData/reducer.ts +++ b/src/store/userAppData/reducer.ts @@ -24,6 +24,12 @@ const reducer = ( processedGiftWraps: action.payload } + case ActionTypes.SET_D_TAG_FOR_APP_DATA: + return { + ...state, + dTag: action.payload + } + case ActionTypes.RESTORE_STATE: return action.payload.userAppData || null diff --git a/src/store/userAppData/types.ts b/src/store/userAppData/types.ts index 07c207f..c3de135 100644 --- a/src/store/userAppData/types.ts +++ b/src/store/userAppData/types.ts @@ -12,7 +12,13 @@ export interface UpdateProcessedGiftWraps { payload: string[] } +export interface SetDTagForAppData { + type: typeof ActionTypes.SET_D_TAG_FOR_APP_DATA + payload: string +} + export type UserAppDataDispatchTypes = | UpdateUserAppData | UpdateProcessedGiftWraps + | SetDTagForAppData | RestoreState diff --git a/src/types/core.ts b/src/types/core.ts index 8583d4a..cb6c2f4 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -43,6 +43,7 @@ export interface UserAppData { sigits: { [key: string]: Meta } // key will be id of create signature processedGiftWraps: string[] // an array of ids of processed gift wrapped events keyPair?: Keys // this key pair is used for blossom requests authentication + dTag?: string // this will be used for d tag in 30078 kind of events blossomUrls: string[] // array for storing Urls for the files that stores all the sigits and processedGiftWraps on blossom } diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index 9bade2f..d65561d 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -11,6 +11,7 @@ import { getEventHash, getPublicKey, kinds, + nip04, nip19, nip44, verifyEvent @@ -23,6 +24,7 @@ import { relayController } from '../controllers' import { + setDTagForAppData, updateProcessedGiftWraps, updateUserAppData as updateUserAppDataAction } from '../store/actions' @@ -30,10 +32,54 @@ import { AuthState, Keys } from '../store/auth/types' import { RelaysState } from '../store/relays/types' import store from '../store/store' import { Meta, SignedEvent, UserAppData } from '../types' -import { getHash } from './hash' +import { getDefaultRelayMap } from './relays' import { parseJson, removeLeadingSlash } from './string' import { timeout } from './utils' -import { getDefaultRelayMap } from './relays' + +/** + * Generates a `d` tag for userAppData + */ +const getDTagForUserAppData = async (): Promise => { + const isLoggedIn = store.getState().auth?.loggedIn + const pubkey = store.getState().auth?.usersPubkey + + if (!isLoggedIn || !pubkey) { + throw new Error( + 'For generating d tag user must be logged in and a valid pubkey should exists in app Store' + ) + } + + let dTag = store.getState().userAppData?.dTag + + // if dTag is found in redux store then just return that + if (dTag) return dTag + + // dTag not found is redux store. Generate it. + const unsignedEvent: UnsignedEvent = { + kind: kinds.ShortTextNote, + pubkey: pubkey, + created_at: 0, + tags: [], + content: `938_${pubkey}` + } + + const nostrController = NostrController.getInstance() + const signedEvent = await nostrController + .signEvent(unsignedEvent) + .catch((err) => { + console.error('Failed to sign event for dTag', err) + toast.error(err.message || err) + return null + }) + if (!signedEvent) return null + + dTag = signedEvent.sig + + // save dTag in redux store + store.dispatch(setDTagForAppData(dTag)) + + return dTag +} /** * @param hexKey hex private or public key @@ -377,13 +423,13 @@ export const getUsersAppData = async (): Promise => { } // Generate an identifier for the user's nip78 - const hash = await getHash('938' + usersPubkey) - if (!hash) return null + const dTag = await getDTagForUserAppData() + if (!dTag) return null // Define a filter for fetching events const filter: Filter = { kinds: [kinds.Application], - '#d': [hash] + '#d': [dTag] } const encryptedContent = await relayController @@ -578,14 +624,14 @@ export const updateUsersAppData = async (meta: Meta) => { if (!encryptedContent) return null // generate the identifier for user's appData event - const hash = await getHash('938' + usersPubkey) - if (!hash) return null + const dTag = await getDTagForUserAppData() + if (!dTag) return null const updatedEvent: UnsignedEvent = { kind: kinds.Application, pubkey: usersPubkey!, created_at: unixNow(), - tags: [['d', hash]], + tags: [['d', dTag]], content: encryptedContent } @@ -693,9 +739,10 @@ const uploadUserAppDataToBlossom = async ( // Convert the private key from hex to bytes const secretKey = hexToBytes(privateKey) // Encrypt the JSON string using the secret key - const encrypted = nip44.v2.encrypt( - stringified, - nip44ConversationKey(secretKey, getPublicKey(secretKey)) + const encrypted = await nip04.encrypt( + secretKey, + getPublicKey(secretKey), + stringified ) // Create a blob from the encrypted data @@ -788,10 +835,7 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => { const pubkey = getPublicKey(secret) // Decrypt the encrypted data using the secret and public key - const decrypted = nip44.v2.decrypt( - encrypted, - nip44ConversationKey(secret, pubkey) - ) + const decrypted = await nip04.decrypt(secret, pubkey, encrypted) // Parse the decrypted JSON content const parsedContent = await parseJson<{ From 73a5d42fcf826de0928e82ba526644ff3087b6dd Mon Sep 17 00:00:00 2001 From: daniyal Date: Thu, 22 Aug 2024 14:37:23 +0500 Subject: [PATCH 02/58] chore(workflow-audit): omit dev dependencies in npm audit --- .gitea/workflows/release-staging.yaml | 2 +- .gitea/workflows/staging-pull-request.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml index 04f4fd7..3e68d43 100644 --- a/.gitea/workflows/release-staging.yaml +++ b/.gitea/workflows/release-staging.yaml @@ -18,7 +18,7 @@ jobs: node-version: 18 - name: Audit - run: npm audit + run: npm audit --omit=dev - name: Install Dependencies run: npm ci diff --git a/.gitea/workflows/staging-pull-request.yaml b/.gitea/workflows/staging-pull-request.yaml index 2bebcd4..cba8164 100644 --- a/.gitea/workflows/staging-pull-request.yaml +++ b/.gitea/workflows/staging-pull-request.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Audit - run: npm audit + run: npm audit --omit=dev - name: Install Dependencies run: npm ci From 49c17149621670b7d44184762ded530fd66efbc1 Mon Sep 17 00:00:00 2001 From: daniyal Date: Thu, 22 Aug 2024 16:11:20 +0500 Subject: [PATCH 03/58] fix: use old approach of using sha256 for generating d tag --- src/store/actionTypes.ts | 1 - src/store/userAppData/action.ts | 11 +---------- src/store/userAppData/reducer.ts | 6 ------ src/store/userAppData/types.ts | 6 ------ src/utils/nostr.ts | 33 ++------------------------------ 5 files changed, 3 insertions(+), 54 deletions(-) diff --git a/src/store/actionTypes.ts b/src/store/actionTypes.ts index ff33627..f0e6698 100644 --- a/src/store/actionTypes.ts +++ b/src/store/actionTypes.ts @@ -18,4 +18,3 @@ export const SET_RELAY_MAP_UPDATED = 'SET_RELAY_MAP_UPDATED' export const UPDATE_USER_APP_DATA = 'UPDATE_USER_APP_DATA' export const UPDATE_PROCESSED_GIFT_WRAPS = 'UPDATE_PROCESSED_GIFT_WRAPS' -export const SET_D_TAG_FOR_APP_DATA = 'SET_D_TAG_FOR_APP_DATA' diff --git a/src/store/userAppData/action.ts b/src/store/userAppData/action.ts index 2164aaa..23bf9a1 100644 --- a/src/store/userAppData/action.ts +++ b/src/store/userAppData/action.ts @@ -1,10 +1,6 @@ import { UserAppData } from '../../types' import * as ActionTypes from '../actionTypes' -import { - SetDTagForAppData, - UpdateProcessedGiftWraps, - UpdateUserAppData -} from './types' +import { UpdateProcessedGiftWraps, UpdateUserAppData } from './types' export const updateUserAppData = (payload: UserAppData): UpdateUserAppData => ({ type: ActionTypes.UPDATE_USER_APP_DATA, @@ -17,8 +13,3 @@ export const updateProcessedGiftWraps = ( type: ActionTypes.UPDATE_PROCESSED_GIFT_WRAPS, payload }) - -export const setDTagForAppData = (payload: string): SetDTagForAppData => ({ - type: ActionTypes.SET_D_TAG_FOR_APP_DATA, - payload -}) diff --git a/src/store/userAppData/reducer.ts b/src/store/userAppData/reducer.ts index dacb375..22c7cb0 100644 --- a/src/store/userAppData/reducer.ts +++ b/src/store/userAppData/reducer.ts @@ -24,12 +24,6 @@ const reducer = ( processedGiftWraps: action.payload } - case ActionTypes.SET_D_TAG_FOR_APP_DATA: - return { - ...state, - dTag: action.payload - } - case ActionTypes.RESTORE_STATE: return action.payload.userAppData || null diff --git a/src/store/userAppData/types.ts b/src/store/userAppData/types.ts index c3de135..07c207f 100644 --- a/src/store/userAppData/types.ts +++ b/src/store/userAppData/types.ts @@ -12,13 +12,7 @@ export interface UpdateProcessedGiftWraps { payload: string[] } -export interface SetDTagForAppData { - type: typeof ActionTypes.SET_D_TAG_FOR_APP_DATA - payload: string -} - export type UserAppDataDispatchTypes = | UpdateUserAppData | UpdateProcessedGiftWraps - | SetDTagForAppData | RestoreState diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index d65561d..eceb8d8 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -24,7 +24,6 @@ import { relayController } from '../controllers' import { - setDTagForAppData, updateProcessedGiftWraps, updateUserAppData as updateUserAppDataAction } from '../store/actions' @@ -35,6 +34,7 @@ import { Meta, SignedEvent, UserAppData } from '../types' import { getDefaultRelayMap } from './relays' import { parseJson, removeLeadingSlash } from './string' import { timeout } from './utils' +import { getHash } from './hash' /** * Generates a `d` tag for userAppData @@ -49,36 +49,7 @@ const getDTagForUserAppData = async (): Promise => { ) } - let dTag = store.getState().userAppData?.dTag - - // if dTag is found in redux store then just return that - if (dTag) return dTag - - // dTag not found is redux store. Generate it. - const unsignedEvent: UnsignedEvent = { - kind: kinds.ShortTextNote, - pubkey: pubkey, - created_at: 0, - tags: [], - content: `938_${pubkey}` - } - - const nostrController = NostrController.getInstance() - const signedEvent = await nostrController - .signEvent(unsignedEvent) - .catch((err) => { - console.error('Failed to sign event for dTag', err) - toast.error(err.message || err) - return null - }) - if (!signedEvent) return null - - dTag = signedEvent.sig - - // save dTag in redux store - store.dispatch(setDTagForAppData(dTag)) - - return dTag + return getHash(`938_${pubkey}`) } /** From 070b02abee5058d8e8e93edc47eb15b0fb8a9bbb Mon Sep 17 00:00:00 2001 From: daniyal Date: Thu, 22 Aug 2024 16:12:58 +0500 Subject: [PATCH 04/58] chore: quick fix --- src/types/core.ts | 1 - src/utils/relays.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/types/core.ts b/src/types/core.ts index cb6c2f4..8583d4a 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -43,7 +43,6 @@ export interface UserAppData { sigits: { [key: string]: Meta } // key will be id of create signature processedGiftWraps: string[] // an array of ids of processed gift wrapped events keyPair?: Keys // this key pair is used for blossom requests authentication - dTag?: string // this will be used for d tag in 30078 kind of events blossomUrls: string[] // array for storing Urls for the files that stores all the sigits and processedGiftWraps on blossom } diff --git a/src/utils/relays.ts b/src/utils/relays.ts index c38767c..5b79f3d 100644 --- a/src/utils/relays.ts +++ b/src/utils/relays.ts @@ -30,7 +30,6 @@ const findRelayListAndUpdateCache = async ( authors: [hexKey] } - console.count('findRelayListAndUpdateCache') const event = await relayController.fetchEvent(eventFilter, lookUpRelays) if (event) { await localCache.addUserRelayListMetadata(event) From 86095cba5c624943c837b6b997f9485411444b01 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 20 Aug 2024 10:28:48 +0200 Subject: [PATCH 05/58] fix(files): show other file types in content for create, fix sign and verify error --- src/pages/sign/index.tsx | 7 +++++-- src/pages/verify/index.tsx | 12 ++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index b8cfe5c..f8f6163 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -416,8 +416,11 @@ export const SignPage = () => { ) if (arrayBuffer) { - files[fileName] = await convertToPdfFile(arrayBuffer, fileName) - + try { + files[fileName] = await convertToPdfFile(arrayBuffer, fileName) + } catch (error) { + console.error('Not a .pdf file:', error) + } const hash = await getHash(arrayBuffer) if (hash) { fileHashes[fileName] = hash diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index c67c2fc..202064b 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -246,10 +246,14 @@ export const VerifyPage = () => { ) if (arrayBuffer) { - files[fileName] = await convertToPdfFile( - arrayBuffer, - fileName! - ) + try { + files[fileName] = await convertToPdfFile( + arrayBuffer, + fileName! + ) + } catch (error) { + console.error('Not a .pdf file:', error) + } const hash = await getHash(arrayBuffer) if (hash) { From c9d7d0a6f58708db866b9099c49800ce48930c65 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 21 Aug 2024 17:07:29 +0200 Subject: [PATCH 06/58] feat(content): show other file types as gray box --- src/components/DrawPDFFields/index.tsx | 4 +- .../DrawPDFFields/style.module.scss | 12 --- src/components/PDFView/PdfItem.tsx | 34 ++++--- src/components/PDFView/index.tsx | 5 +- src/index.css | 12 +++ src/pages/sign/index.tsx | 31 ++++-- src/pages/sign/internal/displayMeta.tsx | 11 ++- src/pages/verify/index.tsx | 95 +++++++++++-------- src/types/file.ts | 2 +- src/utils/file.ts | 16 +++- src/utils/pdf.ts | 11 ++- src/utils/utils.ts | 2 +- 12 files changed, 145 insertions(+), 90 deletions(-) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index a3d43ae..3da227d 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -488,9 +488,7 @@ export const DrawPDFFields = (props: Props) => { {pdfFile ? ( getPdfPages(pdfFile, i) ) : ( -
- This is a {extension} file -
+
This is a {extension} file
)} {i < selectedFiles.length - 1 && ( diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 142f88a..8c888ec 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -104,15 +104,3 @@ flex-direction: column; gap: 25px; } - -.otherFile { - border-radius: 4px; - background: rgba(255, 255, 255, 0.5); - height: 100px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: rgba(0, 0, 0, 0.25); - font-size: 14px; -} diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx index c502bb4..c7f3b54 100644 --- a/src/components/PDFView/PdfItem.tsx +++ b/src/components/PDFView/PdfItem.tsx @@ -1,12 +1,13 @@ import { PdfFile } from '../../types/drawing.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts' +import { extractFileExtension } from '../../utils/meta.ts' import PdfPageItem from './PdfPageItem.tsx' interface PdfItemProps { currentUserMarks: CurrentUserMark[] handleMarkClick: (id: number) => void otherUserMarks: Mark[] - pdfFile: PdfFile + pdfFile: PdfFile | File selectedMark: CurrentUserMark | null selectedMarkValue: string } @@ -31,19 +32,24 @@ const PdfItem = ({ const filterMarksByPage = (marks: Mark[], page: number): Mark[] => { return marks.filter((mark) => mark.location.page === page) } - return pdfFile.pages.map((page, i) => { - return ( - - ) - }) + if ('pages' in pdfFile) { + return pdfFile.pages.map((page, i) => { + return ( + + ) + }) + } else { + const extension = extractFileExtension(pdfFile.name) + return
This is a {extension} file
+ } } export default PdfItem diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx index ef765f0..6af3d69 100644 --- a/src/components/PDFView/index.tsx +++ b/src/components/PDFView/index.tsx @@ -51,11 +51,12 @@ const PdfView = ({ return ( <> {files.map((currentUserFile, index, arr) => { - const { hash, pdfFile, id } = currentUserFile + const { hash, pdfFile, id, filename } = currentUserFile + if (!hash) return return (
(pdfRefs.current[id] = el)} key={index} > diff --git a/src/index.css b/src/index.css index 7ee0eea..b292cb2 100644 --- a/src/index.css +++ b/src/index.css @@ -165,3 +165,15 @@ button:disabled { font-style: normal; font-display: swap; } + +.otherFile { + border-radius: 4px; + background: rgba(255, 255, 255, 0.5); + height: 100px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: rgba(0, 0, 0, 0.25); + font-size: 14px; +} diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index f8f6163..2bc7d49 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -33,13 +33,14 @@ import { sendNotification, signEventForMetaFile, updateUsersAppData, - findOtherUserMarks + findOtherUserMarks, + extractFileExtension } from '../../utils' import { Container } from '../../components/Container' import { DisplayMeta } from './internal/displayMeta' import styles from './style.module.scss' import { PdfFile } from '../../types/drawing.ts' -import { convertToPdfFile } from '../../utils/pdf.ts' +import { convertToPdfFile, toFile } from '../../utils/pdf.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts' import { getLastSignersSig } from '../../utils/sign.ts' import { @@ -76,7 +77,7 @@ export const SignPage = () => { const [selectedFile, setSelectedFile] = useState(null) - const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) + const [files, setFiles] = useState<{ [filename: string]: PdfFile | File }>({}) const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -402,7 +403,7 @@ export const SignPage = () => { return } - const files: { [filename: string]: PdfFile } = {} + const files: { [filename: string]: PdfFile | File } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files).map((entry) => entry.name) @@ -419,7 +420,11 @@ export const SignPage = () => { try { files[fileName] = await convertToPdfFile(arrayBuffer, fileName) } catch (error) { - console.error('Not a .pdf file:', error) + files[fileName] = toFile( + arrayBuffer, + fileName, + 'application/' + extractFileExtension(fileName) + ) } const hash = await getHash(arrayBuffer) if (hash) { @@ -767,8 +772,12 @@ export const SignPage = () => { zip.file('meta.json', stringifiedMeta) - for (const [fileName, pdf] of Object.entries(files)) { - zip.file(`files/${fileName}`, await pdf.file.arrayBuffer()) + for (const [fileName, file] of Object.entries(files)) { + if ('pages' in file) { + zip.file(`files/${fileName}`, await file.file.arrayBuffer()) + } else { + zip.file(`files/${fileName}`, await file.arrayBuffer()) + } } const arrayBuffer = await zip @@ -805,8 +814,12 @@ export const SignPage = () => { zip.file('meta.json', stringifiedMeta) - for (const [fileName, pdf] of Object.entries(files)) { - zip.file(`files/${fileName}`, await pdf.file.arrayBuffer()) + for (const [fileName, file] of Object.entries(files)) { + if ('pages' in file) { + zip.file(`files/${fileName}`, await file.file.arrayBuffer()) + } else { + zip.file(`files/${fileName}`, await file.arrayBuffer()) + } } const arrayBuffer = await zip diff --git a/src/pages/sign/internal/displayMeta.tsx b/src/pages/sign/internal/displayMeta.tsx index 03ba364..8e09612 100644 --- a/src/pages/sign/internal/displayMeta.tsx +++ b/src/pages/sign/internal/displayMeta.tsx @@ -38,7 +38,7 @@ import { PdfFile } from '../../../types/drawing.ts' type DisplayMetaProps = { meta: Meta - files: { [filename: string]: PdfFile } + files: { [filename: string]: PdfFile | File } submittedBy: string signers: `npub1${string}`[] viewers: `npub1${string}`[] @@ -144,7 +144,14 @@ export const DisplayMeta = ({ }, [users, submittedBy, metadata]) const downloadFile = async (filename: string) => { - const arrayBuffer = await files[filename].file.arrayBuffer() + const file = files[filename] + + let arrayBuffer: ArrayBuffer + if ('pages' in file) { + arrayBuffer = await file.file.arrayBuffer() + } else { + arrayBuffer = await file.arrayBuffer() + } if (!arrayBuffer) return const blob = new Blob([arrayBuffer]) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 202064b..24bbc76 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -21,7 +21,8 @@ import { readContentOfZipEntry, signEventForMetaFile, shorten, - getCurrentUserFiles + getCurrentUserFiles, + extractFileExtension } from '../../utils' import styles from './style.module.scss' import { useLocation } from 'react-router-dom' @@ -32,7 +33,8 @@ import { convertToPdfBlob, convertToPdfFile, groupMarksByFileNamePage, - inPx + inPx, + toFile } from '../../utils/pdf.ts' import { State } from '../../store/rootReducer.ts' import { useSelector } from 'react-redux' @@ -85,41 +87,47 @@ const SlimPdfView = ({ ref={(el) => (pdfRefs.current[id] = el)} className={styles.fileWrapper} > - {pdfFile.pages.map((page, i) => { - const marks: Mark[] = [] + {'pages' in pdfFile ? ( + pdfFile.pages.map((page, i) => { + const marks: Mark[] = [] - signatureEvents.forEach((e) => { - const m = parsedSignatureEvents[ - e as `npub1${string}` - ].parsedContent?.marks.filter( - (m) => m.pdfFileHash == hash && m.location.page == i + signatureEvents.forEach((e) => { + const m = parsedSignatureEvents[ + e as `npub1${string}` + ].parsedContent?.marks.filter( + (m) => m.pdfFileHash == hash && m.location.page == i + ) + if (m) { + marks.push(...m) + } + }) + return ( +
+ + {marks.map((m) => { + return ( +
+ {m.value} +
+ ) + })} +
) - if (m) { - marks.push(...m) - } }) - return ( -
- - {marks.map((m) => { - return ( -
- {m.value} -
- ) - })} -
- ) - })} + ) : ( +
+ This is a {extractFileExtension(pdfFile.name)} file +
+ )}
{i < files.length - 1 && ( @@ -171,7 +179,7 @@ export const VerifyPage = () => { const [currentFileHashes, setCurrentFileHashes] = useState<{ [key: string]: string | null }>({}) - const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) + const [files, setFiles] = useState<{ [filename: string]: PdfFile | File }>({}) const [currentFile, setCurrentFile] = useState(null) const [signatureFileHashes, setSignatureFileHashes] = useState<{ [key: string]: string @@ -230,7 +238,7 @@ export const VerifyPage = () => { if (!zip) return - const files: { [filename: string]: PdfFile } = {} + const files: { [filename: string]: PdfFile | File } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files).map( (entry) => entry.name @@ -252,7 +260,11 @@ export const VerifyPage = () => { fileName! ) } catch (error) { - console.error('Not a .pdf file:', error) + files[fileName] = toFile( + arrayBuffer, + fileName, + 'application/' + extractFileExtension(fileName) + ) } const hash = await getHash(arrayBuffer) @@ -428,8 +440,13 @@ export const VerifyPage = () => { const marksByPage = groupMarksByFileNamePage(marks) for (const [fileName, pdf] of Object.entries(files)) { - const pages = await addMarks(pdf.file, marksByPage[fileName]) - const blob = await convertToPdfBlob(pages) + let blob: Blob + if ('pages' in pdf) { + const pages = await addMarks(pdf.file, marksByPage[fileName]) + blob = await convertToPdfBlob(pages) + } else { + blob = new Blob([pdf], { type: pdf.type }) + } zip.file(`files/${fileName}`, blob) } diff --git a/src/types/file.ts b/src/types/file.ts index cb1a7ff..bbb19b6 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -2,7 +2,7 @@ import { PdfFile } from './drawing.ts' export interface CurrentUserFile { id: number - pdfFile: PdfFile + pdfFile: PdfFile | File filename: string hash?: string isHashValid: boolean diff --git a/src/utils/file.ts b/src/utils/file.ts index 401d3c4..c50f2e9 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -6,15 +6,23 @@ import { PdfFile } from '../types/drawing.ts' const getZipWithFiles = async ( meta: Meta, - files: { [filename: string]: PdfFile } + files: { [filename: string]: PdfFile | File } ): Promise => { const zip = new JSZip() const marks = extractMarksFromSignedMeta(meta) const marksByFileNamePage = groupMarksByFileNamePage(marks) - for (const [fileName, pdf] of Object.entries(files)) { - const pages = await addMarks(pdf.file, marksByFileNamePage[fileName]) - const blob = await convertToPdfBlob(pages) + for (const [fileName, file] of Object.entries(files)) { + let blob: Blob + if ('pages' in file) { + // Handle PDF Files + const pages = await addMarks(file.file, marksByFileNamePage[fileName]) + blob = await convertToPdfBlob(pages) + } else { + // Handle other files + blob = new Blob([file], { type: file.type }) + } + zip.file(`files/${fileName}`, blob) } diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index ce2f132..05daa3e 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -30,10 +30,15 @@ const FONT_TYPE: string = 'Arial' * Converts a PDF ArrayBuffer to a generic PDF File * @param arrayBuffer of a PDF * @param fileName identifier of the pdf file + * @param type optional file type (defaults to pdf) */ -const toFile = (arrayBuffer: ArrayBuffer, fileName: string): File => { - const blob = new Blob([arrayBuffer], { type: 'application/pdf' }) - return new File([blob], fileName, { type: 'application/pdf' }) +const toFile = ( + arrayBuffer: ArrayBuffer, + fileName: string, + type: string = 'application/pdf' +): File => { + const blob = new Blob([arrayBuffer], { type }) + return new File([blob], fileName, { type }) } /** diff --git a/src/utils/utils.ts b/src/utils/utils.ts index bde528b..c16e232 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -75,7 +75,7 @@ export const timeout = (ms: number = 60000) => { * @param creatorFileHashes */ export const getCurrentUserFiles = ( - files: { [filename: string]: PdfFile }, + files: { [filename: string]: PdfFile | File }, fileHashes: { [key: string]: string | null }, creatorFileHashes: { [key: string]: string } ): CurrentUserFile[] => { From 05a2dba164f015098cafb29d143b308d8db7dc8a Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 22 Aug 2024 18:20:54 +0200 Subject: [PATCH 07/58] fix: show extension box for non-mark files, de-dupe css and code Closes #138 --- src/App.scss | 58 +++++++++ src/components/DrawPDFFields/index.tsx | 103 +++++++--------- .../DrawPDFFields/style.module.scss | 25 ---- src/components/ExtensionFileBox.tsx | 6 + src/components/FileDivider.tsx | 12 ++ src/components/FileList/index.tsx | 18 +-- src/components/PDFView/PdfItem.tsx | 15 ++- src/components/PDFView/PdfMarking.tsx | 27 ++--- src/components/PDFView/PdfPageItem.tsx | 12 +- src/components/PDFView/index.tsx | 46 +++---- src/components/PDFView/style.module.scss | 26 ---- src/components/UsersDetails.tsx/index.tsx | 2 +- src/controllers/RelayController.ts | 65 +++------- src/index.css | 12 -- src/pages/create/index.tsx | 51 ++++---- src/pages/sign/index.tsx | 41 ++----- src/pages/sign/internal/displayMeta.tsx | 20 +--- src/pages/sign/style.module.scss | 4 +- src/pages/verify/index.tsx | 81 +++++-------- src/pages/verify/style.module.scss | 24 ---- src/types/drawing.ts | 6 - src/types/file.ts | 5 +- src/utils/const.ts | 91 ++++++++++++++ src/utils/file.ts | 112 ++++++++++++++++-- src/utils/meta.ts | 27 +---- src/utils/pdf.ts | 101 ++++------------ src/utils/utils.ts | 37 +++++- 27 files changed, 517 insertions(+), 510 deletions(-) create mode 100644 src/components/ExtensionFileBox.tsx create mode 100644 src/components/FileDivider.tsx diff --git a/src/App.scss b/src/App.scss index 6724890..1b5bc87 100644 --- a/src/App.scss +++ b/src/App.scss @@ -69,3 +69,61 @@ a { input { font-family: inherit; } + +// Shared styles for center content (Create, Sign, Verify) +.files-wrapper { + display: flex; + flex-direction: column; + gap: 25px; +} + +.file-wrapper { + display: flex; + flex-direction: column; + gap: 15px; + position: relative; + + // CSS, scroll position when scrolling to the files is adjusted by + // - 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: $header-height + $body-vertical-padding + 20px; + } + &:not(:first-child) { + scroll-margin-top: $header-height + $body-vertical-padding; + } +} + +.image-wrapper { + position: relative; + -webkit-user-select: none; + user-select: none; + + overflow: hidden; /* Ensure no overflow */ + + > img { + display: block; + max-width: 100%; + max-height: 100%; + object-fit: contain; /* Ensure the image fits within the container */ + } +} + +[data-dev='true'] { + .image-wrapper { + // outline: 1px solid #ccc; /* Optional: for visual debugging */ + background-color: #e0f7fa; /* Optional: for visual debugging */ + } +} + +.extension-file-box { + border-radius: 4px; + background: rgba(255, 255, 255, 0.5); + height: 100px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: rgba(0, 0, 0, 0.25); + font-size: 14px; +} diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 3da227d..cc4d286 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -2,7 +2,6 @@ import { Close } from '@mui/icons-material' import { Box, CircularProgress, - Divider, FormControl, InputLabel, MenuItem, @@ -10,19 +9,15 @@ import { } from '@mui/material' import styles from './style.module.scss' import React, { useEffect, useState } from 'react' - import * as PDFJS from 'pdfjs-dist' import { ProfileMetadata, User, UserRole } from '../../types' -import { - PdfFile, - MouseState, - PdfPage, - DrawnField, - DrawTool -} from '../../types/drawing' +import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing' import { truncate } from 'lodash' -import { extractFileExtension, hexToNpub } from '../../utils' -import { toPdfFiles } from '../../utils/pdf.ts' +import { settleAllFullfilfedPromises, hexToNpub } from '../../utils' +import { getSigitFile, SigitFile } from '../../utils/file' +import { FileDivider } from '../FileDivider' +import { ExtensionFileBox } from '../ExtensionFileBox' + PDFJS.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url @@ -32,15 +27,15 @@ interface Props { selectedFiles: File[] users: User[] metadata: { [key: string]: ProfileMetadata } - onDrawFieldsChange: (pdfFiles: PdfFile[]) => void + onDrawFieldsChange: (sigitFiles: SigitFile[]) => void selectedTool?: DrawTool } export const DrawPDFFields = (props: Props) => { const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props - const [pdfFiles, setPdfFiles] = useState([]) - const [parsingPdf, setParsingPdf] = useState(false) + const [sigitFiles, setSigitFiles] = useState([]) + const [parsingPdf, setIsParsing] = useState(false) const [mouseState, setMouseState] = useState({ clicked: false @@ -49,42 +44,43 @@ export const DrawPDFFields = (props: Props) => { useEffect(() => { if (selectedFiles) { /** - * Reads the pdf binary files and converts it's pages to images - * creates the pdfFiles object and sets to a state + * Reads the binary files and converts to internal file type + * and sets to a state (adds images if it's a PDF) */ - const parsePdfPages = async () => { - const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles) + const parsePages = async () => { + const files = await settleAllFullfilfedPromises( + selectedFiles, + getSigitFile + ) - setPdfFiles(pdfFiles) + setSigitFiles(files) } - setParsingPdf(true) + setIsParsing(true) - parsePdfPages().finally(() => { - setParsingPdf(false) + parsePages().finally(() => { + setIsParsing(false) }) } }, [selectedFiles]) useEffect(() => { - if (pdfFiles) onDrawFieldsChange(pdfFiles) - }, [onDrawFieldsChange, pdfFiles]) + if (sigitFiles) onDrawFieldsChange(sigitFiles) + }, [onDrawFieldsChange, sigitFiles]) /** * Drawing events */ useEffect(() => { - // window.addEventListener('mousedown', onMouseDown); window.addEventListener('mouseup', onMouseUp) return () => { - // window.removeEventListener('mousedown', onMouseDown); window.removeEventListener('mouseup', onMouseUp) } }, []) const refreshPdfFiles = () => { - setPdfFiles([...pdfFiles]) + setSigitFiles([...sigitFiles]) } /** @@ -303,10 +299,10 @@ export const DrawPDFFields = (props: Props) => { ) => { event.stopPropagation() - pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice( - drawnFileIndex, - 1 - ) + const pages = sigitFiles[pdfFileIndex]?.pages + if (pages) { + pages[pdfPageIndex]?.drawnFields?.splice(drawnFileIndex, 1) + } } /** @@ -345,14 +341,17 @@ export const DrawPDFFields = (props: Props) => { /** * Renders the pdf pages and drawing elements */ - const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => { + const getPdfPages = (file: SigitFile, fileIndex: number) => { + // Early return if this is not a pdf + if (!file.isPdf) return null + return ( <> - {pdfFile.pages.map((page, pdfPageIndex: number) => { + {file.pages?.map((page, pageIndex: number) => { return (
{ @@ -393,8 +392,8 @@ export const DrawPDFFields = (props: Props) => { onMouseDown={(event) => { onRemoveHandleMouseDown( event, - pdfFileIndex, - pdfPageIndex, + fileIndex, + pageIndex, drawnFieldIndex ) }} @@ -469,38 +468,24 @@ export const DrawPDFFields = (props: Props) => { ) } - if (!pdfFiles.length) { + if (!sigitFiles.length) { return '' } return ( -
- {selectedFiles.map((file, i) => { +
+ {sigitFiles.map((file, i) => { const name = file.name - const extension = extractFileExtension(name) - const pdfFile = pdfFiles.find((pdf) => pdf.file.name === name) return ( -
- {pdfFile ? ( - getPdfPages(pdfFile, i) +
+ {file.isPdf ? ( + getPdfPages(file, i) ) : ( -
This is a {extension} file
+ )}
- {i < selectedFiles.length - 1 && ( - - File Separator - - )} + {i < selectedFiles.length - 1 && } ) })} diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 8c888ec..83844ce 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -8,17 +8,6 @@ } .pdfImageWrapper { - position: relative; - -webkit-user-select: none; - user-select: none; - - > img { - display: block; - max-width: 100%; - max-height: 100%; - object-fit: contain; /* Ensure the image fits within the container */ - } - &.drawing { cursor: crosshair; } @@ -90,17 +79,3 @@ padding: 5px 0; } } - -.fileWrapper { - display: flex; - flex-direction: column; - gap: 15px; - position: relative; - scroll-margin-top: $header-height + $body-vertical-padding; -} - -.view { - display: flex; - flex-direction: column; - gap: 25px; -} diff --git a/src/components/ExtensionFileBox.tsx b/src/components/ExtensionFileBox.tsx new file mode 100644 index 0000000..f36d38c --- /dev/null +++ b/src/components/ExtensionFileBox.tsx @@ -0,0 +1,6 @@ +interface ExtensionFileBoxProps { + extension: string +} +export const ExtensionFileBox = ({ extension }: ExtensionFileBoxProps) => ( +
This is a {extension} file
+) diff --git a/src/components/FileDivider.tsx b/src/components/FileDivider.tsx new file mode 100644 index 0000000..b66b8f4 --- /dev/null +++ b/src/components/FileDivider.tsx @@ -0,0 +1,12 @@ +import Divider from '@mui/material/Divider/Divider' + +export const FileDivider = () => ( + + File Separator + +) diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx index 53557a5..38fab88 100644 --- a/src/components/FileList/index.tsx +++ b/src/components/FileList/index.tsx @@ -24,19 +24,23 @@ const FileList = ({
    - {files.map((file: CurrentUserFile) => ( + {files.map((currentUserFile: CurrentUserFile) => (
  • setCurrentFile(file)} + key={currentUserFile.id} + className={`${styles.fileItem} ${isActive(currentUserFile) && styles.active}`} + onClick={() => setCurrentFile(currentUserFile)} > -
    {file.id}
    +
    {currentUserFile.id}
    -
    {file.filename}
    +
    + {currentUserFile.file.name} +
    - {file.isHashValid && } + {currentUserFile.isHashValid && ( + + )}
  • ))} diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx index c7f3b54..b6707d3 100644 --- a/src/components/PDFView/PdfItem.tsx +++ b/src/components/PDFView/PdfItem.tsx @@ -1,13 +1,13 @@ -import { PdfFile } from '../../types/drawing.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts' -import { extractFileExtension } from '../../utils/meta.ts' +import { SigitFile } from '../../utils/file.ts' +import { ExtensionFileBox } from '../ExtensionFileBox.tsx' import PdfPageItem from './PdfPageItem.tsx' interface PdfItemProps { currentUserMarks: CurrentUserMark[] handleMarkClick: (id: number) => void otherUserMarks: Mark[] - pdfFile: PdfFile | File + file: SigitFile selectedMark: CurrentUserMark | null selectedMarkValue: string } @@ -16,7 +16,7 @@ interface PdfItemProps { * Responsible for displaying pages of a single Pdf File. */ const PdfItem = ({ - pdfFile, + file, currentUserMarks, handleMarkClick, selectedMarkValue, @@ -32,8 +32,8 @@ const PdfItem = ({ const filterMarksByPage = (marks: Mark[], page: number): Mark[] => { return marks.filter((mark) => mark.location.page === page) } - if ('pages' in pdfFile) { - return pdfFile.pages.map((page, i) => { + if (file.isPdf) { + return file.pages?.map((page, i) => { return ( This is a {extension} file
+ return } } diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index 9fff924..dafe42b 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -10,7 +10,6 @@ import { import { EMPTY } from '../../utils/const.ts' import { Container } from '../Container' import signPageStyles from '../../pages/sign/style.module.scss' -import styles from './style.module.scss' import { CurrentUserFile } from '../../types/file.ts' import FileList from '../FileList' import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' @@ -134,21 +133,17 @@ const PdfMarking = (props: PdfMarkingProps) => { } right={meta !== null && } > -
- {currentUserMarks?.length > 0 && ( -
- -
- )} -
+ {currentUserMarks?.length > 0 && ( + + )} {selectedMark !== null && ( { if (selectedMark !== null && !!markRefs.current[selectedMark.id]) { markRefs.current[selectedMark.id]?.scrollIntoView({ - behavior: 'smooth', - block: 'end' + behavior: 'smooth' }) } }, [selectedMark]) const markRefs = useRef<(HTMLDivElement | null)[]>([]) return ( -
- +
+ {currentUserMarks.map((m, i) => (
(markRefs.current[m.id] = el)}> ([]) useEffect(() => { if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { - pdfRefs.current[currentFile.id]?.scrollIntoView({ - behavior: 'smooth', - block: 'end' - }) + pdfRefs.current[currentFile.id]?.scrollIntoView({ behavior: 'smooth' }) } }, [currentFile]) const filterByFile = ( @@ -49,30 +47,32 @@ const PdfView = ({ const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean => index !== files.length - 1 return ( - <> +
{files.map((currentUserFile, index, arr) => { - const { hash, pdfFile, id, filename } = currentUserFile + const { hash, file, id } = currentUserFile if (!hash) return return ( -
(pdfRefs.current[id] = el)} - key={index} - > - - {isNotLastPdfFile(index, arr) && File Separator} -
+ +
(pdfRefs.current[id] = el)} + > + +
+ {isNotLastPdfFile(index, arr) && } +
) })} - +
) } diff --git a/src/components/PDFView/style.module.scss b/src/components/PDFView/style.module.scss index 3a893d4..870057a 100644 --- a/src/components/PDFView/style.module.scss +++ b/src/components/PDFView/style.module.scss @@ -1,33 +1,7 @@ -.imageWrapper { - display: flex; - justify-content: center; - align-items: center; - width: 100%; /* Adjust as needed */ - height: 100%; /* Adjust as needed */ - overflow: hidden; /* Ensure no overflow */ - border: 1px solid #ccc; /* Optional: for visual debugging */ - background-color: #e0f7fa; /* Optional: for visual debugging */ -} - -.image { - max-width: 100%; - max-height: 100%; - object-fit: contain; /* Ensure the image fits within the container */ -} - .container { display: flex; width: 100%; flex-direction: column; - -} - -.pdfView { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - gap: 10px; } .otherUserMarksDisplay { diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx index 3681cfd..16ff440 100644 --- a/src/components/UsersDetails.tsx/index.tsx +++ b/src/components/UsersDetails.tsx/index.tsx @@ -1,7 +1,6 @@ import { Divider, Tooltip } from '@mui/material' import { useSigitProfiles } from '../../hooks/useSigitProfiles' import { - extractFileExtensions, formatTimestamp, fromUnixTimestamp, hexToNpub, @@ -28,6 +27,7 @@ import { State } from '../../store/rootReducer' import { TooltipChild } from '../TooltipChild' import { DisplaySigner } from '../DisplaySigner' import { Meta } from '../../types' +import { extractFileExtensions } from '../../utils/file' interface UsersDetailsProps { meta: Meta diff --git a/src/controllers/RelayController.ts b/src/controllers/RelayController.ts index 40945ad..df33b4b 100644 --- a/src/controllers/RelayController.ts +++ b/src/controllers/RelayController.ts @@ -1,5 +1,9 @@ import { Event, Filter, Relay } from 'nostr-tools' -import { normalizeWebSocketURL, timeout } from '../utils' +import { + settleAllFullfilfedPromises, + normalizeWebSocketURL, + timeout +} from '../utils' import { SIGIT_RELAY } from '../utils/const' /** @@ -105,24 +109,11 @@ export class RelayController { } // connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => - this.connectRelay(relayUrl) + const relays = await settleAllFullfilfedPromises( + relayUrls, + this.connectRelay ) - // Use Promise.allSettled to wait for all promises to settle - const results = await Promise.allSettled(relayPromises) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) - // Check if any relays are connected if (relays.length === 0) { throw new Error('No relay is connected to fetch events!') @@ -228,23 +219,10 @@ export class RelayController { } // connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => { - return this.connectRelay(relayUrl) - }) - - // Use Promise.allSettled to wait for all promises to settle - const results = await Promise.allSettled(relayPromises) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) + const relays = await settleAllFullfilfedPromises( + relayUrls, + this.connectRelay + ) // Check if any relays are connected if (relays.length === 0) { @@ -292,24 +270,11 @@ export class RelayController { } // connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => - this.connectRelay(relayUrl) + const relays = await settleAllFullfilfedPromises( + relayUrls, + this.connectRelay ) - // Use Promise.allSettled to wait for all promises to settle - const results = await Promise.allSettled(relayPromises) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) - // Check if any relays are connected if (relays.length === 0) { throw new Error('No relay is connected to publish event!') diff --git a/src/index.css b/src/index.css index b292cb2..7ee0eea 100644 --- a/src/index.css +++ b/src/index.css @@ -165,15 +165,3 @@ button:disabled { font-style: normal; font-display: swap; } - -.otherFile { - border-radius: 4px; - background: rgba(255, 255, 255, 0.5); - height: 100px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: rgba(0, 0, 0, 0.25); - font-size: 14px; -} diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 8a73012..2bcd063 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -51,7 +51,7 @@ import { import { Container } from '../../components/Container' import styles from './style.module.scss' import fileListStyles from '../../components/FileList/style.module.scss' -import { DrawTool, MarkType, PdfFile } 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' @@ -83,6 +83,7 @@ import { faTrash, faUpload } from '@fortawesome/free-solid-svg-icons' +import { SigitFile } from '../../utils/file.ts' export const CreatePage = () => { const navigate = useNavigate() @@ -125,7 +126,7 @@ export const CreatePage = () => { const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( {} ) - const [drawnPdfs, setDrawnPdfs] = useState([]) + const [drawnFiles, setDrawnFiles] = useState([]) const [selectedTool, setSelectedTool] = useState() const [toolbox] = useState([ @@ -507,26 +508,28 @@ export const CreatePage = () => { } const createMarks = (fileHashes: { [key: string]: string }): Mark[] => { - return drawnPdfs - .flatMap((drawnPdf) => { - const fileHash = fileHashes[drawnPdf.file.name] - return drawnPdf.pages.flatMap((page, index) => { - return page.drawnFields.map((drawnField) => { - return { - type: drawnField.type, - location: { - page: index, - top: drawnField.top, - left: drawnField.left, - height: drawnField.height, - width: drawnField.width - }, - npub: drawnField.counterpart, - pdfFileHash: fileHash, - fileName: drawnPdf.file.name - } - }) - }) + return drawnFiles + .flatMap((file) => { + const fileHash = fileHashes[file.name] + return ( + file.pages?.flatMap((page, index) => { + return page.drawnFields.map((drawnField) => { + return { + type: drawnField.type, + location: { + page: index, + top: drawnField.top, + left: drawnField.left, + height: drawnField.height, + width: drawnField.width + }, + npub: drawnField.counterpart, + pdfFileHash: fileHash, + fileName: file.name + } + }) + }) || [] + ) }) .map((mark, index) => { return { ...mark, id: index } @@ -846,8 +849,8 @@ export const CreatePage = () => { } } - const onDrawFieldsChange = (pdfFiles: PdfFile[]) => { - setDrawnPdfs(pdfFiles) + const onDrawFieldsChange = (sigitFiles: SigitFile[]) => { + setDrawnFiles(sigitFiles) } if (authUrl) { diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 2bc7d49..0e0b135 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -33,14 +33,11 @@ import { sendNotification, signEventForMetaFile, updateUsersAppData, - findOtherUserMarks, - extractFileExtension + findOtherUserMarks } from '../../utils' import { Container } from '../../components/Container' import { DisplayMeta } from './internal/displayMeta' import styles from './style.module.scss' -import { PdfFile } from '../../types/drawing.ts' -import { convertToPdfFile, toFile } from '../../utils/pdf.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts' import { getLastSignersSig } from '../../utils/sign.ts' import { @@ -50,7 +47,11 @@ import { updateMarks } from '../../utils' import PdfMarking from '../../components/PDFView/PdfMarking.tsx' -import { getZipWithFiles } from '../../utils/file.ts' +import { + convertToSigitFile, + getZipWithFiles, + SigitFile +} from '../../utils/file.ts' import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts' enum SignedStatus { Fully_Signed, @@ -77,7 +78,7 @@ export const SignPage = () => { const [selectedFile, setSelectedFile] = useState(null) - const [files, setFiles] = useState<{ [filename: string]: PdfFile | File }>({}) + const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({}) const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -403,7 +404,7 @@ export const SignPage = () => { return } - const files: { [filename: string]: PdfFile | File } = {} + const files: { [filename: string]: SigitFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files).map((entry) => entry.name) @@ -417,15 +418,7 @@ export const SignPage = () => { ) if (arrayBuffer) { - try { - files[fileName] = await convertToPdfFile(arrayBuffer, fileName) - } catch (error) { - files[fileName] = toFile( - arrayBuffer, - fileName, - 'application/' + extractFileExtension(fileName) - ) - } + files[fileName] = await convertToSigitFile(arrayBuffer, fileName) const hash = await getHash(arrayBuffer) if (hash) { fileHashes[fileName] = hash @@ -470,7 +463,7 @@ export const SignPage = () => { const zip = await loadZip(decryptedZipFile) if (!zip) return - const files: { [filename: string]: PdfFile } = {} + const files: { [filename: string]: SigitFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files) .filter((entry) => entry.name.startsWith('files/') && !entry.dir) @@ -487,7 +480,7 @@ export const SignPage = () => { ) if (arrayBuffer) { - files[fileName] = await convertToPdfFile(arrayBuffer, fileName) + files[fileName] = await convertToSigitFile(arrayBuffer, fileName) const hash = await getHash(arrayBuffer) if (hash) { @@ -773,11 +766,7 @@ export const SignPage = () => { zip.file('meta.json', stringifiedMeta) for (const [fileName, file] of Object.entries(files)) { - if ('pages' in file) { - zip.file(`files/${fileName}`, await file.file.arrayBuffer()) - } else { - zip.file(`files/${fileName}`, await file.arrayBuffer()) - } + zip.file(`files/${fileName}`, await file.arrayBuffer()) } const arrayBuffer = await zip @@ -815,11 +804,7 @@ export const SignPage = () => { zip.file('meta.json', stringifiedMeta) for (const [fileName, file] of Object.entries(files)) { - if ('pages' in file) { - zip.file(`files/${fileName}`, await file.file.arrayBuffer()) - } else { - zip.file(`files/${fileName}`, await file.arrayBuffer()) - } + zip.file(`files/${fileName}`, await file.arrayBuffer()) } const arrayBuffer = await zip diff --git a/src/pages/sign/internal/displayMeta.tsx b/src/pages/sign/internal/displayMeta.tsx index 8e09612..bb298c6 100644 --- a/src/pages/sign/internal/displayMeta.tsx +++ b/src/pages/sign/internal/displayMeta.tsx @@ -34,11 +34,11 @@ import { UserAvatar } from '../../../components/UserAvatar' import { MetadataController } from '../../../controllers' import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils' import styles from '../style.module.scss' -import { PdfFile } from '../../../types/drawing.ts' +import { SigitFile } from '../../../utils/file' type DisplayMetaProps = { meta: Meta - files: { [filename: string]: PdfFile | File } + files: { [fileName: string]: SigitFile } submittedBy: string signers: `npub1${string}`[] viewers: `npub1${string}`[] @@ -143,19 +143,9 @@ export const DisplayMeta = ({ }) }, [users, submittedBy, metadata]) - const downloadFile = async (filename: string) => { - const file = files[filename] - - let arrayBuffer: ArrayBuffer - if ('pages' in file) { - arrayBuffer = await file.file.arrayBuffer() - } else { - arrayBuffer = await file.arrayBuffer() - } - if (!arrayBuffer) return - - const blob = new Blob([arrayBuffer]) - saveAs(blob, filename) + const downloadFile = async (fileName: string) => { + const file = files[fileName] + saveAs(file) } return ( diff --git a/src/pages/sign/style.module.scss b/src/pages/sign/style.module.scss index 1dbc6c5..dffb039 100644 --- a/src/pages/sign/style.module.scss +++ b/src/pages/sign/style.module.scss @@ -2,8 +2,6 @@ .container { color: $text-color; - //width: 550px; - //max-width: 550px; .inputBlock { position: relative; @@ -67,7 +65,7 @@ //z-index: 200; } - .fixedBottomForm input[type="text"] { + .fixedBottomForm input[type='text'] { width: 80%; padding: 10px; font-size: 16px; diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 24bbc76..172ce42 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Divider, Tooltip, Typography } from '@mui/material' +import { Box, Button, Tooltip, Typography } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' import { Event, verifyEvent } from 'nostr-tools' @@ -21,20 +21,16 @@ import { readContentOfZipEntry, signEventForMetaFile, shorten, - getCurrentUserFiles, - extractFileExtension + getCurrentUserFiles } from '../../utils' import styles from './style.module.scss' import { useLocation } from 'react-router-dom' import axios from 'axios' -import { PdfFile } from '../../types/drawing.ts' import { addMarks, convertToPdfBlob, - convertToPdfFile, groupMarksByFileNamePage, - inPx, - toFile + inPx } from '../../utils/pdf.ts' import { State } from '../../store/rootReducer.ts' import { useSelector } from 'react-redux' @@ -51,6 +47,9 @@ import FileList from '../../components/FileList' import { CurrentUserFile } from '../../types/file.ts' import { Mark } from '../../types/mark.ts' import React from 'react' +import { convertToSigitFile, SigitFile } from '../../utils/file.ts' +import { FileDivider } from '../../components/FileDivider.tsx' +import { ExtensionFileBox } from '../../components/ExtensionFileBox.tsx' interface PdfViewProps { files: CurrentUserFile[] @@ -69,26 +68,25 @@ const SlimPdfView = ({ useEffect(() => { if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { pdfRefs.current[currentFile.id]?.scrollIntoView({ - behavior: 'smooth', - block: 'end' + behavior: 'smooth' }) } }, [currentFile]) return ( -
+
{files.map((currentUserFile, i) => { - const { hash, filename, pdfFile, id } = currentUserFile + const { hash, file, id } = currentUserFile const signatureEvents = Object.keys(parsedSignatureEvents) if (!hash) return return ( - +
(pdfRefs.current[id] = el)} - className={styles.fileWrapper} + className="file-wrapper" > - {'pages' in pdfFile ? ( - pdfFile.pages.map((page, i) => { + {file.isPdf ? ( + file.pages?.map((page, i) => { const marks: Mark[] = [] signatureEvents.forEach((e) => { @@ -102,7 +100,7 @@ const SlimPdfView = ({ } }) return ( -
+
{marks.map((m) => { return ( @@ -124,22 +122,11 @@ const SlimPdfView = ({ ) }) ) : ( -
- This is a {extractFileExtension(pdfFile.name)} file -
+ )}
- {i < files.length - 1 && ( - - File Separator - - )} + {i < files.length - 1 && } ) })} @@ -179,7 +166,7 @@ export const VerifyPage = () => { const [currentFileHashes, setCurrentFileHashes] = useState<{ [key: string]: string | null }>({}) - const [files, setFiles] = useState<{ [filename: string]: PdfFile | File }>({}) + const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({}) const [currentFile, setCurrentFile] = useState(null) const [signatureFileHashes, setSignatureFileHashes] = useState<{ [key: string]: string @@ -238,7 +225,7 @@ export const VerifyPage = () => { if (!zip) return - const files: { [filename: string]: PdfFile | File } = {} + const files: { [fileName: string]: SigitFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files).map( (entry) => entry.name @@ -254,18 +241,10 @@ export const VerifyPage = () => { ) if (arrayBuffer) { - try { - files[fileName] = await convertToPdfFile( - arrayBuffer, - fileName! - ) - } catch (error) { - files[fileName] = toFile( - arrayBuffer, - fileName, - 'application/' + extractFileExtension(fileName) - ) - } + files[fileName] = await convertToSigitFile( + arrayBuffer, + fileName! + ) const hash = await getHash(arrayBuffer) if (hash) { @@ -439,15 +418,15 @@ export const VerifyPage = () => { const marks = extractMarksFromSignedMeta(updatedMeta) const marksByPage = groupMarksByFileNamePage(marks) - for (const [fileName, pdf] of Object.entries(files)) { - let blob: Blob - if ('pages' in pdf) { - const pages = await addMarks(pdf.file, marksByPage[fileName]) - blob = await convertToPdfBlob(pages) + for (const [fileName, file] of Object.entries(files)) { + if (file.isPdf) { + // Draw marks into PDF file and generate a brand new blob + const pages = await addMarks(file, marksByPage[fileName]) + const blob = await convertToPdfBlob(pages) + zip.file(`files/${fileName}`, blob) } else { - blob = new Blob([pdf], { type: pdf.type }) + zip.file(`files/${fileName}`, file) } - zip.file(`files/${fileName}`, blob) } const arrayBuffer = await zip diff --git a/src/pages/verify/style.module.scss b/src/pages/verify/style.module.scss index a3d3401..af93107 100644 --- a/src/pages/verify/style.module.scss +++ b/src/pages/verify/style.module.scss @@ -51,30 +51,6 @@ } } -.view { - width: 550px; - max-width: 550px; - - display: flex; - flex-direction: column; - gap: 25px; -} - -.imageWrapper { - position: relative; - - img { - width: 100%; - display: block; - } -} - -.fileWrapper { - display: flex; - flex-direction: column; - gap: 15px; -} - .mark { position: absolute; diff --git a/src/types/drawing.ts b/src/types/drawing.ts index 1e65038..b8abe73 100644 --- a/src/types/drawing.ts +++ b/src/types/drawing.ts @@ -8,12 +8,6 @@ export interface MouseState { } } -export interface PdfFile { - file: File - pages: PdfPage[] - expanded?: boolean -} - export interface PdfPage { image: string drawnFields: DrawnField[] diff --git a/src/types/file.ts b/src/types/file.ts index bbb19b6..4553b23 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -1,9 +1,8 @@ -import { PdfFile } from './drawing.ts' +import { SigitFile } from '../utils/file' export interface CurrentUserFile { id: number - pdfFile: PdfFile | File - filename: string + file: SigitFile hash?: string isHashValid: boolean } diff --git a/src/utils/const.ts b/src/utils/const.ts index 2940399..83aca9e 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -26,3 +26,94 @@ export const DEFAULT_LOOK_UP_RELAY_LIST = [ 'wss://user.kindpag.es', 'wss://purplepag.es' ] + +// Uses the https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types list +// Updated on 2024/08/22 +export const MOST_COMMON_MEDIA_TYPES = new Map([ + ['aac', 'audio/aac'], // AAC audio + ['abw', 'application/x-abiword'], // AbiWord document + ['apng', 'image/apng'], // Animated Portable Network Graphics (APNG) image + ['arc', 'application/x-freearc'], // Archive document (multiple files embedded) + ['avif', 'image/avif'], // AVIF image + ['avi', 'video/x-msvideo'], // AVI: Audio Video Interleave + ['azw', 'application/vnd.amazon.ebook'], // Amazon Kindle eBook format + ['bin', 'application/octet-stream'], // Any kind of binary data + ['bmp', 'image/bmp'], // Windows OS/2 Bitmap Graphics + ['bz', 'application/x-bzip'], // BZip archive + ['bz2', 'application/x-bzip2'], // BZip2 archive + ['cda', 'application/x-cdf'], // CD audio + ['csh', 'application/x-csh'], // C-Shell script + ['css', 'text/css'], // Cascading Style Sheets (CSS) + ['csv', 'text/csv'], // Comma-separated values (CSV) + ['doc', 'application/msword'], // Microsoft Word + [ + 'docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + ], // Microsoft Word (OpenXML) + ['eot', 'application/vnd.ms-fontobject'], // MS Embedded OpenType fonts + ['epub', 'application/epub+zip'], // Electronic publication (EPUB) + ['gz', 'application/gzip'], // GZip Compressed Archive + ['gif', 'image/gif'], // Graphics Interchange Format (GIF) + ['htm', 'text/html'], // HyperText Markup Language (HTML) + ['html', 'text/html'], // HyperText Markup Language (HTML) + ['ico', 'image/vnd.microsoft.icon'], // Icon format + ['ics', 'text/calendar'], // iCalendar format + ['jar', 'application/java-archive'], // Java Archive (JAR) + ['jpeg', 'image/jpeg'], // JPEG images + ['jpg', 'image/jpeg'], // JPEG images + ['js', 'text/javascript'], // JavaScript + ['json', 'application/json'], // JSON format + ['jsonld', 'application/ld+json'], // JSON-LD format + ['mid', 'audio/midi'], // Musical Instrument Digital Interface (MIDI) + ['midi', 'audio/midi'], // Musical Instrument Digital Interface (MIDI) + ['mjs', 'text/javascript'], // JavaScript module + ['mp3', 'audio/mpeg'], // MP3 audio + ['mp4', 'video/mp4'], // MP4 video + ['mpeg', 'video/mpeg'], // MPEG Video + ['mpkg', 'application/vnd.apple.installer+xml'], // Apple Installer Package + ['odp', 'application/vnd.oasis.opendocument.presentation'], // OpenDocument presentation document + ['ods', 'application/vnd.oasis.opendocument.spreadsheet'], // OpenDocument spreadsheet document + ['odt', 'application/vnd.oasis.opendocument.text'], // OpenDocument text document + ['oga', 'audio/ogg'], // Ogg audio + ['ogv', 'video/ogg'], // Ogg video + ['ogx', 'application/ogg'], // Ogg + ['opus', 'audio/ogg'], // Opus audio in Ogg container + ['otf', 'font/otf'], // OpenType font + ['png', 'image/png'], // Portable Network Graphics + ['pdf', 'application/pdf'], // Adobe Portable Document Format (PDF) + ['php', 'application/x-httpd-php'], // Hypertext Preprocessor (Personal Home Page) + ['ppt', 'application/vnd.ms-powerpoint'], // Microsoft PowerPoint + [ + 'pptx', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + ], // Microsoft PowerPoint (OpenXML) + ['rar', 'application/vnd.rar'], // RAR archive + ['rtf', 'application/rtf'], // Rich Text Format (RTF) + ['sh', 'application/x-sh'], // Bourne shell script + ['svg', 'image/svg+xml'], // Scalable Vector Graphics (SVG) + ['tar', 'application/x-tar'], // Tape Archive (TAR) + ['tif', 'image/tiff'], // Tagged Image File Format (TIFF) + ['tiff', 'image/tiff'], // Tagged Image File Format (TIFF) + ['ts', 'video/mp2t'], // MPEG transport stream + ['ttf', 'font/ttf'], // TrueType Font + ['txt', 'text/plain'], // Text, (generally ASCII or ISO 8859-n) + ['vsd', 'application/vnd.visio'], // Microsoft Visio + ['wav', 'audio/wav'], // Waveform Audio Format + ['weba', 'audio/webm'], // WEBM audio + ['webm', 'video/webm'], // WEBM video + ['webp', 'image/webp'], // WEBP image + ['woff', 'font/woff'], // Web Open Font Format (WOFF) + ['woff2', 'font/woff2'], // Web Open Font Format (WOFF) + ['xhtml', 'application/xhtml+xml'], // XHTML + ['xls', 'application/vnd.ms-excel'], // Microsoft Excel + [ + '.xlsx', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ], // Microsoft Excel (OpenXML) + ['xml', 'application/xml'], // XML + ['xul', 'application/vnd.mozilla.xul+xml'], // XUL + ['zip', 'application/zip'], // ZIP archive + ['3gp', 'video/3gpp'], // 3GPP audio/video container + ['3g2', 'video/3gpp2'], // 3GPP2 audio/video container + ['7z', 'application/x-7z-compressed'] // 7-zip archive +]) diff --git a/src/utils/file.ts b/src/utils/file.ts index c50f2e9..d84524d 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,32 +1,120 @@ import { Meta } from '../types' +import { PdfPage } from '../types/drawing.ts' +import { MOST_COMMON_MEDIA_TYPES } from './const.ts' import { extractMarksFromSignedMeta } from './mark.ts' -import { addMarks, convertToPdfBlob, groupMarksByFileNamePage } from './pdf.ts' +import { + addMarks, + convertToPdfBlob, + groupMarksByFileNamePage, + isPdf, + pdfToImages +} from './pdf.ts' import JSZip from 'jszip' -import { PdfFile } from '../types/drawing.ts' -const getZipWithFiles = async ( +export const getZipWithFiles = async ( meta: Meta, - files: { [filename: string]: PdfFile | File } + files: { [filename: string]: SigitFile } ): Promise => { const zip = new JSZip() const marks = extractMarksFromSignedMeta(meta) const marksByFileNamePage = groupMarksByFileNamePage(marks) for (const [fileName, file] of Object.entries(files)) { - let blob: Blob - if ('pages' in file) { + if (file.isPdf) { // Handle PDF Files - const pages = await addMarks(file.file, marksByFileNamePage[fileName]) - blob = await convertToPdfBlob(pages) + const pages = await addMarks(file, marksByFileNamePage[fileName]) + const blob = await convertToPdfBlob(pages) + zip.file(`files/${fileName}`, blob) } else { // Handle other files - blob = new Blob([file], { type: file.type }) + zip.file(`files/${fileName}`, file) } - - zip.file(`files/${fileName}`, blob) } return zip } -export { getZipWithFiles } +/** + * Converts a PDF ArrayBuffer to a generic PDF File + * @param arrayBuffer of a PDF + * @param fileName identifier of the pdf file + * @param type optional file type (defaults to pdf) + */ +export const toFile = ( + arrayBuffer: ArrayBuffer, + fileName: string, + type: string = 'application/pdf' +): File => { + const blob = new Blob([arrayBuffer], { type }) + return new File([blob], fileName, { type }) +} + +export class SigitFile extends File { + extension: string + pages?: PdfPage[] + isPdf: boolean + + constructor(file: File) { + super([file], file.name, { type: file.type }) + this.isPdf = isPdf(this) + this.extension = extractFileExtension(this.name) + } + + async process() { + if (this.isPdf) this.pages = await pdfToImages(await this.arrayBuffer()) + } +} + +export const getSigitFile = async (file: File) => { + const sigitFile = new SigitFile(file) + // Process sigit file + // - generate pages for PDF files + await sigitFile.process() + return sigitFile +} + +/** + * Takes an ArrayBuffer and converts to Sigit's Internal File type + * @param arrayBuffer + * @param fileName + */ +export const convertToSigitFile = async ( + arrayBuffer: ArrayBuffer, + fileName: string +): Promise => { + const type = getMediaType(extractFileExtension(fileName)) + const file = toFile(arrayBuffer, fileName, type) + const sigitFile = await getSigitFile(file) + return sigitFile +} + +/** + * @param fileNames - List of filenames to check + * @returns List of extensions and if all are same + */ +export const extractFileExtensions = (fileNames: string[]) => { + const extensions = fileNames.reduce((result: string[], file: string) => { + const extension = file.split('.').pop() + if (extension) { + result.push(extension) + } + return result + }, []) + + const isSame = extensions.every((ext) => ext === extensions[0]) + + return { extensions, isSame } +} + +/** + * @param fileName - Filename to check + * @returns Extension string + */ +export const extractFileExtension = (fileName: string) => { + const parts = fileName.split('.') + return parts[parts.length - 1] +} + +export const getMediaType = (extension: string) => { + return MOST_COMMON_MEDIA_TYPES.get(extension) +} diff --git a/src/utils/meta.ts b/src/utils/meta.ts index 0bee969..fd3481c 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -2,6 +2,7 @@ import { CreateSignatureEventContent, Meta } from '../types' import { fromUnixTimestamp, parseJson } from '.' import { Event, verifyEvent } from 'nostr-tools' import { toast } from 'react-toastify' +import { extractFileExtensions } from './file' export enum SignStatus { Signed = 'Signed', @@ -172,29 +173,3 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { } } } - -/** - * @param fileNames - List of filenames to check - * @returns List of extensions and if all are same - */ -export const extractFileExtensions = (fileNames: string[]) => { - const extensions = fileNames.reduce((result: string[], file: string) => { - const extension = file.split('.').pop() - if (extension) { - result.push(extension) - } - return result - }, []) - - const isSame = extensions.every((ext) => ext === extensions[0]) - - return { extensions, isSame } -} - -/** - * @param fileName - Filename to check - * @returns Extension string - */ -export const extractFileExtension = (fileName: string) => { - return fileName.split('.').pop() -} diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index 05daa3e..8abafae 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -1,4 +1,4 @@ -import { PdfFile, PdfPage } from '../types/drawing.ts' +import { PdfPage } from '../types/drawing.ts' import * as PDFJS from 'pdfjs-dist' import { PDFDocument } from 'pdf-lib' import { Mark } from '../types/mark.ts' @@ -12,7 +12,7 @@ PDFJS.GlobalWorkerOptions.workerSrc = new URL( * Scale between the PDF page's natural size and rendered size * @constant {number} */ -const SCALE: number = 3 +export const SCALE: number = 3 /** * 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 @@ -20,63 +20,28 @@ const SCALE: number = 3 * This should be fixed going forward. * Switching to PDF-Lib will most likely make this problem redundant. */ -const FONT_SIZE: number = 40 +export const FONT_SIZE: number = 40 /** * Current font type used when generating a PDF. */ -const FONT_TYPE: string = 'Arial' - -/** - * Converts a PDF ArrayBuffer to a generic PDF File - * @param arrayBuffer of a PDF - * @param fileName identifier of the pdf file - * @param type optional file type (defaults to pdf) - */ -const toFile = ( - arrayBuffer: ArrayBuffer, - fileName: string, - type: string = 'application/pdf' -): File => { - const blob = new Blob([arrayBuffer], { type }) - return new File([blob], fileName, { type }) -} - -/** - * Converts a generic PDF File to Sigit's internal Pdf File type - * @param {File} file - * @return {PdfFile} Sigit's internal PDF File type - */ -const toPdfFile = async (file: File): Promise => { - const data = await readPdf(file) - const pages = await pdfToImages(data) - return { file, pages, expanded: false } -} -/** - * Transforms an array of generic PDF Files into an array of Sigit's - * internal representation of Pdf Files - * @param selectedFiles - an array of generic PDF Files - * @return PdfFile[] - an array of Sigit's internal Pdf File type - */ -const toPdfFiles = async (selectedFiles: File[]): Promise => { - return Promise.all(selectedFiles.filter(isPdf).map(toPdfFile)) -} +export const FONT_TYPE: string = 'Arial' /** * A utility that transforms a drawing coordinate number into a CSS-compatible string * @param coordinate */ -const inPx = (coordinate: number): string => `${coordinate}px` +export const inPx = (coordinate: number): string => `${coordinate}px` /** * A utility that checks if a given file is of the pdf type * @param file */ -const isPdf = (file: File) => file.type.toLowerCase().includes('pdf') +export const isPdf = (file: File) => file.type.toLowerCase().includes('pdf') /** * Reads the pdf file binaries */ -const readPdf = (file: File): Promise => { +export const readPdf = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader() @@ -104,7 +69,9 @@ const readPdf = (file: File): Promise => { * Converts pdf to the images * @param data pdf file bytes */ -const pdfToImages = async (data: string | ArrayBuffer): Promise => { +export const pdfToImages = async ( + data: string | ArrayBuffer +): Promise => { const images: string[] = [] const pdf = await PDFJS.getDocument(data).promise const canvas = document.createElement('canvas') @@ -134,7 +101,7 @@ const pdfToImages = async (data: string | ArrayBuffer): Promise => { * Returns an array of encoded images where each image is a representation * of a PDF page with completed and signed marks from all users */ -const addMarks = async ( +export const addMarks = async ( file: File, marksPerPage: { [key: string]: Mark[] } ) => { @@ -164,7 +131,7 @@ const addMarks = async ( /** * Utility to scale mark in line with the PDF-to-PNG scale */ -const scaleMark = (mark: Mark): Mark => { +export const scaleMark = (mark: Mark): Mark => { const { location } = mark return { ...mark, @@ -182,14 +149,14 @@ const scaleMark = (mark: Mark): Mark => { * Utility to check if a Mark has value * @param mark */ -const hasValue = (mark: Mark): boolean => !!mark.value +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 */ -const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { +export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { const { location } = mark ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE @@ -204,7 +171,9 @@ const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { * Takes an array of encoded PDF pages and returns a blob that is a complete PDF file * @param markedPdfPages */ -const convertToPdfBlob = async (markedPdfPages: string[]): Promise => { +export const convertToPdfBlob = async ( + markedPdfPages: string[] +): Promise => { const pdfDoc = await PDFDocument.create() for (const page of markedPdfPages) { @@ -222,30 +191,17 @@ const convertToPdfBlob = async (markedPdfPages: string[]): Promise => { return new Blob([pdfBytes], { type: 'application/pdf' }) } -/** - * Takes an ArrayBuffer of a PDF file and converts to Sigit's Internal Pdf File type - * @param arrayBuffer - * @param fileName - */ -const convertToPdfFile = async ( - arrayBuffer: ArrayBuffer, - fileName: string -): Promise => { - const file = toFile(arrayBuffer, fileName) - return toPdfFile(file) -} - /** * @param marks - an array of Marks * @function hasValue removes any Mark without a property * @function scaleMark scales remaining marks in line with SCALE * @function byPage groups remaining Marks by their page marks.location.page */ -const groupMarksByFileNamePage = (marks: Mark[]) => { +export const groupMarksByFileNamePage = (marks: Mark[]) => { return marks .filter(hasValue) .map(scaleMark) - .reduce<{ [filename: string]: { [page: number]: Mark[] } }>(byPage, {}) + .reduce<{ [fileName: string]: { [page: number]: Mark[] } }>(byPage, {}) } /** @@ -256,30 +212,19 @@ const groupMarksByFileNamePage = (marks: Mark[]) => { * @param obj - accumulator in the reducer callback * @param mark - current value, i.e. Mark being examined */ -const byPage = ( +export const byPage = ( obj: { [filename: string]: { [page: number]: Mark[] } }, mark: Mark ) => { - const filename = mark.fileName + const fileName = mark.fileName const pageNumber = mark.location.page - const pages = obj[filename] ?? {} + const pages = obj[fileName] ?? {} const marks = pages[pageNumber] ?? [] return { ...obj, - [filename]: { + [fileName]: { ...pages, [pageNumber]: [...marks, mark] } } } - -export { - toFile, - toPdfFile, - toPdfFiles, - inPx, - convertToPdfFile, - addMarks, - convertToPdfBlob, - groupMarksByFileNamePage -} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index c16e232..f32e14e 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,5 +1,5 @@ -import { PdfFile } from '../types/drawing.ts' import { CurrentUserFile } from '../types/file.ts' +import { SigitFile } from './file.ts' export const compareObjects = ( obj1: object | null | undefined, @@ -75,13 +75,13 @@ export const timeout = (ms: number = 60000) => { * @param creatorFileHashes */ export const getCurrentUserFiles = ( - files: { [filename: string]: PdfFile | File }, + files: { [filename: string]: SigitFile }, fileHashes: { [key: string]: string | null }, creatorFileHashes: { [key: string]: string } ): CurrentUserFile[] => { - return Object.entries(files).map(([filename, pdfFile], index) => { + return Object.entries(files).map(([filename, file], index) => { return { - pdfFile, + file, filename, id: index + 1, ...(!!fileHashes[filename] && { hash: fileHashes[filename]! }), @@ -89,3 +89,32 @@ export const getCurrentUserFiles = ( } }) } + +/** + * Utility function that generates a promise with a callback on each array item + * and retuns only non-null fulfilled results + * @param array + * @param cb callback that generates a promise + * @returns Array with the non-null results + */ +export const settleAllFullfilfedPromises = async ( + array: Item[], + cb: (arg: Item) => Promise +) => { + // Run the callback on the array to get promises + const promises = array.map(cb) + + // Use Promise.allSettled to wait for all promises to settle + const results = await Promise.allSettled(promises) + + // Extract non-null values from fulfilled promises in a single pass + return results.reduce[]>((acc, result) => { + if (result.status === 'fulfilled') { + const value = result.value + if (value) { + acc.push(value) + } + } + return acc + }, []) +} From ae08b07d7404bb8a9988a700ebce72d777e2d725 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 22 Aug 2024 18:48:03 +0200 Subject: [PATCH 08/58] feat: add uploaded image file as preview --- src/App.scss | 12 ++++++++++++ src/components/DrawPDFFields/index.tsx | 17 +++++++++++------ src/components/PDFView/PdfItem.tsx | 2 ++ src/pages/verify/index.tsx | 14 ++++++++++---- src/utils/file.ts | 21 ++++++++++++++++++++- 5 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/App.scss b/src/App.scss index 1b5bc87..f21738d 100644 --- a/src/App.scss +++ b/src/App.scss @@ -94,6 +94,7 @@ input { } } +// For pdf marks .image-wrapper { position: relative; -webkit-user-select: none; @@ -109,6 +110,17 @@ input { } } +// For image rendering (uploaded image as a file) +.file-image { + -webkit-user-select: none; + user-select: none; + + display: block; + width: 100%; + height: auto; + object-fit: contain; /* Ensure the image fits within the container */ +} + [data-dev='true'] { .image-wrapper { // outline: 1px solid #ccc; /* Optional: for visual debugging */ diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index cc4d286..b720dad 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -475,13 +475,18 @@ export const DrawPDFFields = (props: Props) => { return (
{sigitFiles.map((file, i) => { - const name = file.name return ( - -
- {file.isPdf ? ( - getPdfPages(file, i) - ) : ( + +
+ {file.isPdf && getPdfPages(file, i)} + {file.isImage && ( + {file.name} + )} + {!(file.isPdf || file.isImage) && ( )}
diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx index b6707d3..1a6c6a6 100644 --- a/src/components/PDFView/PdfItem.tsx +++ b/src/components/PDFView/PdfItem.tsx @@ -46,6 +46,8 @@ const PdfItem = ({ /> ) }) + } else if (file.isImage) { + return {file.name} } else { return } diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 172ce42..4e21a82 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -85,7 +85,7 @@ const SlimPdfView = ({ ref={(el) => (pdfRefs.current[id] = el)} className="file-wrapper" > - {file.isPdf ? ( + {file.isPdf && file.pages?.map((page, i) => { const marks: Mark[] = [] @@ -120,12 +120,18 @@ const SlimPdfView = ({ })}
) - }) - ) : ( + })} + {file.isImage && ( + {file.name} + )} + {!(file.isPdf || file.isImage) && ( )}
- {i < files.length - 1 && } ) diff --git a/src/utils/file.ts b/src/utils/file.ts index d84524d..aad31c7 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -51,17 +51,22 @@ export const toFile = ( export class SigitFile extends File { extension: string - pages?: PdfPage[] isPdf: boolean + isImage: boolean + + pages?: PdfPage[] + objectUrl?: string constructor(file: File) { super([file], file.name, { type: file.type }) this.isPdf = isPdf(this) + this.isImage = isImage(this) this.extension = extractFileExtension(this.name) } async process() { if (this.isPdf) this.pages = await pdfToImages(await this.arrayBuffer()) + if (this.isImage) this.objectUrl = URL.createObjectURL(this) } } @@ -118,3 +123,17 @@ export const extractFileExtension = (fileName: string) => { export const getMediaType = (extension: string) => { return MOST_COMMON_MEDIA_TYPES.get(extension) } + +export const isImage = (file: File) => { + const validImageMediaTypes = [ + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/gif', + 'image/svg+xml', + 'image/bmp', + 'image/x-icon' + ] + + return validImageMediaTypes.includes(file.type.toLowerCase()) +} From acc8c84617a38443d301804f540f5d40b4a0d461 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 22 Aug 2024 18:58:30 +0200 Subject: [PATCH 09/58] fix(home-page): sigit file type display now correctly shows multiple file types --- src/components/DisplaySigit/index.tsx | 31 ++++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index 92dc01d..473a942 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -10,7 +10,8 @@ import { faCalendar, faCopy, faEye, - faFile + faFile, + faFileCircleExclamation } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { UserAvatarGroup } from '../UserAvatarGroup' @@ -20,6 +21,7 @@ import { TooltipChild } from '../TooltipChild' import { getExtensionIconLabel } from '../getExtensionIconLabel' import { useSigitProfiles } from '../../hooks/useSigitProfiles' import { useSigitMeta } from '../../hooks/useSigitMeta' +import { extractFileExtensions } from '../../utils/file' type SigitProps = { meta: Meta @@ -27,23 +29,18 @@ type SigitProps = { } export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { - const { - title, - createdAt, - submittedBy, - signers, - signedStatus, - fileExtensions, - isValid - } = parsedMeta + const { title, createdAt, submittedBy, signers, signedStatus, isValid } = + parsedMeta - const { signersStatus } = useSigitMeta(meta) + const { signersStatus, fileHashes } = useSigitMeta(meta) const profiles = useSigitProfiles([ ...(submittedBy ? [submittedBy] : []), ...signers ]) + const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes)) + return (
{ {signedStatus} - {fileExtensions.length > 0 ? ( + {extensions.length > 0 ? ( - {fileExtensions.length > 1 ? ( + {!isSame ? ( <> Multiple File Types ) : ( - getExtensionIconLabel(fileExtensions[0]) + getExtensionIconLabel(extensions[0]) )} - ) : null} + ) : ( + <> + — + + )}
From 2f5418462584ac5426c717680590d9156801ec57 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 23 Aug 2024 10:09:29 +0200 Subject: [PATCH 10/58] fix(drawing): clamp DrawField within img Closes #154 --- src/components/DrawPDFFields/index.tsx | 9 +++++++-- src/components/DrawPDFFields/style.module.scss | 1 - 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index b720dad..e5e33ca 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -328,8 +328,10 @@ export const DrawPDFFields = (props: Props) => { ) => { const target = customTarget ? customTarget : event.currentTarget const rect = target.getBoundingClientRect() - const mouseX = event.clientX - rect.left //x position within the element. - const mouseY = event.clientY - rect.top //y position within the element. + + // Clamp X Y within the target + const mouseX = Math.min(event.clientX, rect.right) - rect.left //x position within the element. + const mouseY = Math.min(event.clientY, rect.bottom) - rect.top //y position within the element. return { mouseX, @@ -415,6 +417,9 @@ export const DrawPDFFields = (props: Props) => { }} labelId="counterparts" label="Counterparts" + sx={{ + background: 'white' + }} > {users .filter((u) => u.role === UserRole.signer) diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 83844ce..b3150b3 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -75,7 +75,6 @@ bottom: -60px; min-width: 170px; min-height: 30px; - background: #fff; padding: 5px 0; } } From 31f36750cd5479fc05e2a86ade5153e6a65955f6 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 23 Aug 2024 11:06:25 +0200 Subject: [PATCH 11/58] fix(pdf): font style consistency --- src/components/DrawPDFFields/style.module.scss | 4 ++++ src/components/PDFView/PdfMarkItem.tsx | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index b3150b3..d0085df 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -23,6 +23,10 @@ justify-content: center; align-items: center; + font-family: Arial; + font-size: 16px; + font-weight: normal; + &.nonEditable { cursor: default; visibility: hidden; diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx index d93c2b2..138c5e1 100644 --- a/src/components/PDFView/PdfMarkItem.tsx +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -1,6 +1,6 @@ import { CurrentUserMark } from '../../types/mark.ts' import styles from '../DrawPDFFields/style.module.scss' -import { inPx } from '../../utils/pdf.ts' +import { FONT_TYPE, inPx } from '../../utils/pdf.ts' interface PdfMarkItemProps { userMark: CurrentUserMark @@ -31,7 +31,8 @@ const PdfMarkItem = ({ left: inPx(location.left), top: inPx(location.top), width: inPx(location.width), - height: inPx(location.height) + height: inPx(location.height), + fontFamily: FONT_TYPE }} > {getMarkValue()} From ac3186a02ed441c6efc31aaf462a5b8b229f5fa1 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 23 Aug 2024 13:39:49 +0200 Subject: [PATCH 12/58] fix(pdf): scaling and font styles consistency Closes #146 --- src/App.scss | 10 +++ .../DrawPDFFields/style.module.scss | 4 - src/components/PDFView/PdfMarkItem.tsx | 7 +- src/pages/verify/index.tsx | 10 ++- src/pages/verify/style.module.scss | 2 +- src/types/drawing.ts | 1 + src/utils/file.ts | 6 +- src/utils/pdf.ts | 83 +++++++++++-------- 8 files changed, 78 insertions(+), 45 deletions(-) diff --git a/src/App.scss b/src/App.scss index f21738d..a1c267a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -121,6 +121,16 @@ input { object-fit: contain; /* Ensure the image fits within the container */ } +// Consistent styling for every file mark +// Reverts some of the design defaults for font +.file-mark { + font-family: Arial; + font-size: 16px; + font-weight: normal; + color: black; + letter-spacing: normal; +} + [data-dev='true'] { .image-wrapper { // outline: 1px solid #ccc; /* Optional: for visual debugging */ diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index d0085df..b3150b3 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -23,10 +23,6 @@ justify-content: center; align-items: center; - font-family: Arial; - font-size: 16px; - font-weight: normal; - &.nonEditable { cursor: default; visibility: hidden; diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx index 138c5e1..9642499 100644 --- a/src/components/PDFView/PdfMarkItem.tsx +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -1,6 +1,6 @@ import { CurrentUserMark } from '../../types/mark.ts' import styles from '../DrawPDFFields/style.module.scss' -import { FONT_TYPE, inPx } from '../../utils/pdf.ts' +import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts' interface PdfMarkItemProps { userMark: CurrentUserMark @@ -26,13 +26,14 @@ const PdfMarkItem = ({ return (
{getMarkValue()} diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 4e21a82..f442cd7 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -29,6 +29,8 @@ import axios from 'axios' import { addMarks, convertToPdfBlob, + FONT_SIZE, + FONT_TYPE, groupMarksByFileNamePage, inPx } from '../../utils/pdf.ts' @@ -105,13 +107,15 @@ const SlimPdfView = ({ {marks.map((m) => { return (
{m.value} @@ -427,7 +431,7 @@ 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 pages = await addMarks(file, marksByPage[fileName]) + const pages = await addMarks(file, file.pages!, marksByPage[fileName]) const blob = await convertToPdfBlob(pages) zip.file(`files/${fileName}`, blob) } else { diff --git a/src/pages/verify/style.module.scss b/src/pages/verify/style.module.scss index af93107..b63ba60 100644 --- a/src/pages/verify/style.module.scss +++ b/src/pages/verify/style.module.scss @@ -61,6 +61,6 @@ [data-dev='true'] { .mark { - border: 1px dotted black; + outline: 1px dotted black; } } diff --git a/src/types/drawing.ts b/src/types/drawing.ts index b8abe73..bb7c970 100644 --- a/src/types/drawing.ts +++ b/src/types/drawing.ts @@ -10,6 +10,7 @@ export interface MouseState { export interface PdfPage { image: string + scale: number drawnFields: DrawnField[] } diff --git a/src/utils/file.ts b/src/utils/file.ts index aad31c7..26f03ef 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -22,7 +22,11 @@ export const getZipWithFiles = async ( for (const [fileName, file] of Object.entries(files)) { if (file.isPdf) { // Handle PDF Files - const pages = await addMarks(file, marksByFileNamePage[fileName]) + const pages = await addMarks( + file, + file.pages!, + marksByFileNamePage[fileName] + ) const blob = await convertToPdfBlob(pages) zip.file(`files/${fileName}`, blob) } else { diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index 8abafae..c7eca36 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -8,11 +8,16 @@ PDFJS.GlobalWorkerOptions.workerSrc = new URL( import.meta.url ).toString() +/** + * Default width of the rendered element on the website + * @constant {number} + */ +export const DEFAULT_VIEWPORT_WIDTH = 550 /** * Scale between the PDF page's natural size and rendered size * @constant {number} */ -export const SCALE: number = 3 +export const DEFAULT_SCALE: number = 1 /** * 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 @@ -20,7 +25,7 @@ export const SCALE: number = 3 * This should be fixed going forward. * Switching to PDF-Lib will most likely make this problem redundant. */ -export const FONT_SIZE: number = 40 +export const FONT_SIZE: number = 16 /** * Current font type used when generating a PDF. */ @@ -72,28 +77,30 @@ export const readPdf = (file: File): Promise => { export const pdfToImages = async ( data: string | ArrayBuffer ): Promise => { - const images: string[] = [] + const pages: PdfPage[] = [] const pdf = await PDFJS.getDocument(data).promise const canvas = document.createElement('canvas') for (let i = 0; i < pdf.numPages; i++) { const page = await pdf.getPage(i + 1) - const viewport = page.getViewport({ scale: SCALE }) + + const originalViewport = page.getViewport({ scale: 1 }) + const scale = originalViewport.width / DEFAULT_VIEWPORT_WIDTH + + const viewport = page.getViewport({ scale }) const context = canvas.getContext('2d') canvas.height = viewport.height canvas.width = viewport.width + await page.render({ canvasContext: context!, viewport: viewport }).promise - images.push(canvas.toDataURL()) + pages.push({ + image: canvas.toDataURL(), + scale, + drawnFields: [] + }) } - return Promise.resolve( - images.map((image) => { - return { - image, - drawnFields: [] - } - }) - ) + return pages } /** @@ -103,6 +110,7 @@ export const pdfToImages = async ( */ export const addMarks = async ( file: File, + pages: PdfPage[], marksPerPage: { [key: string]: Mark[] } ) => { const p = await readPdf(file) @@ -113,34 +121,39 @@ export const addMarks = async ( for (let i = 0; i < pdf.numPages; i++) { const page = await pdf.getPage(i + 1) - const viewport = page.getViewport({ scale: SCALE }) + const viewport = page.getViewport({ scale: 1 }) const context = canvas.getContext('2d') canvas.height = viewport.height canvas.width = viewport.width - await page.render({ canvasContext: context!, viewport: viewport }).promise + if (context) { + await page.render({ canvasContext: context, viewport: viewport }).promise - if (marksPerPage && Object.hasOwn(marksPerPage, i)) - marksPerPage[i]?.forEach((mark) => draw(mark, context!)) + if (marksPerPage && Object.hasOwn(marksPerPage, i)) { + marksPerPage[i]?.forEach((mark) => draw(mark, context, pages[i].scale)) + } - images.push(canvas.toDataURL()) + images.push(canvas.toDataURL()) + } } - return Promise.resolve(images) + canvas.remove() + + return images } /** * Utility to scale mark in line with the PDF-to-PNG scale */ -export const scaleMark = (mark: Mark): Mark => { +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 + width: location.width * scale, + height: location.height * scale, + left: location.left * scale, + top: location.top * scale } } } @@ -156,15 +169,21 @@ export const hasValue = (mark: Mark): boolean => !!mark.value * @param mark to be drawn * @param ctx a Canvas representation of a specific PDF Page */ -export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { - const { location } = mark +export const draw = ( + mark: Mark, + ctx: CanvasRenderingContext2D, + scale: number +) => { + const { location } = scaleMark(mark, scale) - ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE - ctx!.fillStyle = 'black' - const textMetrics = ctx!.measureText(mark.value!) + ctx.font = scale * FONT_SIZE + 'px ' + FONT_TYPE + ctx.fillStyle = 'black' + const textMetrics = ctx.measureText(mark.value!) + const textHeight = + textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent const textX = location.left + (location.width - textMetrics.width) / 2 - const textY = location.top + (location.height + parseInt(ctx!.font)) / 2 - ctx!.fillText(mark.value!, textX, textY) + const textY = location.top + (location.height + textHeight) / 2 + ctx.fillText(mark.value!, textX, textY) } /** @@ -194,13 +213,11 @@ export const convertToPdfBlob = async ( /** * @param marks - an array of Marks * @function hasValue removes any Mark without a property - * @function scaleMark scales remaining marks in line with SCALE * @function byPage groups remaining Marks by their page marks.location.page */ export const groupMarksByFileNamePage = (marks: Mark[]) => { return marks .filter(hasValue) - .map(scaleMark) .reduce<{ [fileName: string]: { [page: number]: Mark[] } }>(byPage, {}) } From ecc1707212fdee75775d38154ffbddc22619fb88 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 23 Aug 2024 13:49:47 +0200 Subject: [PATCH 13/58] fix(pdf): add border to style --- src/App.scss | 1 + src/pages/verify/style.module.scss | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/App.scss b/src/App.scss index a1c267a..1b8ae10 100644 --- a/src/App.scss +++ b/src/App.scss @@ -129,6 +129,7 @@ input { font-weight: normal; color: black; letter-spacing: normal; + border: 1px solid transparent; } [data-dev='true'] { diff --git a/src/pages/verify/style.module.scss b/src/pages/verify/style.module.scss index b63ba60..af93107 100644 --- a/src/pages/verify/style.module.scss +++ b/src/pages/verify/style.module.scss @@ -61,6 +61,6 @@ [data-dev='true'] { .mark { - outline: 1px dotted black; + border: 1px dotted black; } } From ea09daa6692e905c703d3800b8a8adbdb391f6b5 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 23 Aug 2024 21:30:32 +0200 Subject: [PATCH 14/58] fix(pdf): dynamic mark scaling Caveat: scaling is not fluid, refresh is required --- src/App.scss | 6 ++-- src/components/DrawPDFFields/index.tsx | 39 ++++++++++++----------- src/components/PDFView/PdfMarkItem.tsx | 14 ++++---- src/components/PDFView/PdfPageItem.tsx | 35 +++++++++++--------- src/layouts/StickySideColumns.module.scss | 4 --- src/layouts/StickySideColumns.tsx | 4 ++- src/pages/verify/index.tsx | 12 +++---- src/types/drawing.ts | 8 ++--- src/types/mark.ts | 13 +++++--- src/utils/file.ts | 7 ++-- src/utils/pdf.ts | 33 ++++++++----------- 11 files changed, 87 insertions(+), 88 deletions(-) diff --git a/src/App.scss b/src/App.scss index 1b8ae10..b24e16a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -100,12 +100,10 @@ input { -webkit-user-select: none; user-select: none; - overflow: hidden; /* Ensure no overflow */ - > img { display: block; - max-width: 100%; - max-height: 100%; + width: 100%; + height: auto; object-fit: contain; /* Ensure the image fits within the container */ } } diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index e5e33ca..35de444 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -17,6 +17,7 @@ import { settleAllFullfilfedPromises, hexToNpub } from '../../utils' import { getSigitFile, SigitFile } from '../../utils/file' import { FileDivider } from '../FileDivider' import { ExtensionFileBox } from '../ExtensionFileBox' +import { inPx } from '../../utils/pdf' PDFJS.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -105,8 +106,8 @@ export const DrawPDFFields = (props: Props) => { const { mouseX, mouseY } = getMouseCoordinates(event) const newField: DrawnField = { - left: mouseX, - top: mouseY, + left: mouseX / page.scale, + top: mouseY / page.scale, width: 0, height: 0, counterpart: '', @@ -160,8 +161,8 @@ export const DrawPDFFields = (props: Props) => { const { mouseX, mouseY } = getMouseCoordinates(event) - const width = mouseX - lastDrawnField.left - const height = mouseY - lastDrawnField.top + const width = mouseX / page.scale - lastDrawnField.left + const height = mouseY / page.scale - lastDrawnField.top lastDrawnField.width = width lastDrawnField.height = height @@ -209,7 +210,8 @@ export const DrawPDFFields = (props: Props) => { */ const onDrawnFieldMouseMove = ( event: React.MouseEvent, - drawnField: DrawnField + drawnField: DrawnField, + scale: number ) => { if (mouseState.dragging) { const { mouseX, mouseY, rect } = getMouseCoordinates( @@ -219,11 +221,11 @@ export const DrawPDFFields = (props: Props) => { const coordsOffset = mouseState.coordsInWrapper if (coordsOffset) { - let left = mouseX - coordsOffset.mouseX - let top = mouseY - coordsOffset.mouseY + let left = (mouseX - coordsOffset.mouseX) / scale + let top = (mouseY - coordsOffset.mouseY) / scale - const rightLimit = rect.width - drawnField.width - 3 - const bottomLimit = rect.height - drawnField.height - 3 + const rightLimit = rect.width / scale - drawnField.width - 3 + const bottomLimit = rect.height / scale - drawnField.height - 3 if (left < 0) left = 0 if (top < 0) top = 0 @@ -263,7 +265,8 @@ export const DrawPDFFields = (props: Props) => { */ const onResizeHandleMouseMove = ( event: React.MouseEvent, - drawnField: DrawnField + drawnField: DrawnField, + scale: number ) => { if (mouseState.resizing) { const { mouseX, mouseY } = getMouseCoordinates( @@ -274,8 +277,8 @@ export const DrawPDFFields = (props: Props) => { event.currentTarget.parentElement?.parentElement ) - const width = mouseX - drawnField.left - const height = mouseY - drawnField.top + const width = mouseX / scale - drawnField.left + const height = mouseY / scale - drawnField.top drawnField.width = width drawnField.height = height @@ -372,21 +375,21 @@ export const DrawPDFFields = (props: Props) => { key={drawnFieldIndex} onMouseDown={onDrawnFieldMouseDown} onMouseMove={(event) => { - onDrawnFieldMouseMove(event, drawnField) + onDrawnFieldMouseMove(event, drawnField, page.scale) }} className={styles.drawingRectangle} style={{ - left: `${drawnField.left}px`, - top: `${drawnField.top}px`, - width: `${drawnField.width}px`, - height: `${drawnField.height}px`, + left: inPx(drawnField.left * page.scale), + top: inPx(drawnField.top * page.scale), + width: inPx(drawnField.width * page.scale), + height: inPx(drawnField.height * page.scale), pointerEvents: mouseState.clicked ? 'none' : 'all' }} > { - onResizeHandleMouseMove(event, drawnField) + onResizeHandleMouseMove(event, drawnField, page.scale) }} className={styles.resizeHandle} > diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx index 9642499..cc9c9b2 100644 --- a/src/components/PDFView/PdfMarkItem.tsx +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -7,6 +7,7 @@ interface PdfMarkItemProps { handleMarkClick: (id: number) => void selectedMarkValue: string selectedMark: CurrentUserMark | null + scale: number } /** @@ -16,7 +17,8 @@ const PdfMarkItem = ({ selectedMark, handleMarkClick, selectedMarkValue, - userMark + userMark, + scale }: PdfMarkItemProps) => { const { location } = userMark.mark const handleClick = () => handleMarkClick(userMark.mark.id) @@ -28,12 +30,12 @@ const PdfMarkItem = ({ onClick={handleClick} className={`file-mark ${styles.drawingRectangle} ${isEdited() && styles.edited}`} style={{ - left: inPx(location.left), - top: inPx(location.top), - width: inPx(location.width), - height: inPx(location.height), + left: inPx(location.left * scale), + top: inPx(location.top * scale), + width: inPx(location.width * scale), + height: inPx(location.height * scale), fontFamily: FONT_TYPE, - fontSize: inPx(FONT_SIZE) + fontSize: inPx(FONT_SIZE * scale) }} > {getMarkValue()} diff --git a/src/components/PDFView/PdfPageItem.tsx b/src/components/PDFView/PdfPageItem.tsx index 65ea322..f5f310d 100644 --- a/src/components/PDFView/PdfPageItem.tsx +++ b/src/components/PDFView/PdfPageItem.tsx @@ -4,7 +4,7 @@ import { CurrentUserMark, Mark } from '../../types/mark.ts' import PdfMarkItem from './PdfMarkItem.tsx' import { useEffect, useRef } from 'react' import pdfViewStyles from './style.module.scss' -import { inPx } from '../../utils/pdf.ts' +import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts' interface PdfPageProps { currentUserMarks: CurrentUserMark[] handleMarkClick: (id: number) => void @@ -44,23 +44,28 @@ const PdfPageItem = ({ selectedMarkValue={selectedMarkValue} userMark={m} selectedMark={selectedMark} + scale={page.scale} />
))} - {otherUserMarks.map((m, i) => ( -
- {m.value} -
- ))} + {otherUserMarks.map((m, i) => { + return ( +
+ {m.value} +
+ ) + })}
) } diff --git a/src/layouts/StickySideColumns.module.scss b/src/layouts/StickySideColumns.module.scss index 7495cad..77daa03 100644 --- a/src/layouts/StickySideColumns.module.scss +++ b/src/layouts/StickySideColumns.module.scss @@ -29,8 +29,4 @@ padding: 10px; border: 10px solid $overlay-background-color; border-radius: 4px; - - max-width: 590px; - width: 590px; - margin: 0 auto; } diff --git a/src/layouts/StickySideColumns.tsx b/src/layouts/StickySideColumns.tsx index 1ada87f..43dc430 100644 --- a/src/layouts/StickySideColumns.tsx +++ b/src/layouts/StickySideColumns.tsx @@ -17,7 +17,9 @@ export const StickySideColumns = ({
{left}
-
{children}
+
+ {children} +
{right}
diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index f442cd7..6ff679a 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -110,12 +110,12 @@ const SlimPdfView = ({ className={`file-mark ${styles.mark}`} key={m.id} style={{ - left: inPx(m.location.left), - top: inPx(m.location.top), - width: inPx(m.location.width), - height: inPx(m.location.height), + left: inPx(m.location.left * page.scale), + top: inPx(m.location.top * page.scale), + width: inPx(m.location.width * page.scale), + height: inPx(m.location.height * page.scale), fontFamily: FONT_TYPE, - fontSize: inPx(FONT_SIZE) + fontSize: inPx(FONT_SIZE * page.scale) }} > {m.value} @@ -431,7 +431,7 @@ 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 pages = await addMarks(file, file.pages!, marksByPage[fileName]) + const pages = await addMarks(file, marksByPage[fileName]) const blob = await convertToPdfBlob(pages) zip.file(`files/${fileName}`, blob) } else { diff --git a/src/types/drawing.ts b/src/types/drawing.ts index bb7c970..d8c5cc3 100644 --- a/src/types/drawing.ts +++ b/src/types/drawing.ts @@ -1,3 +1,5 @@ +import { MarkRect } from './mark' + export interface MouseState { clicked?: boolean dragging?: boolean @@ -14,11 +16,7 @@ export interface PdfPage { drawnFields: DrawnField[] } -export interface DrawnField { - left: number - top: number - width: number - height: number +export interface DrawnField extends MarkRect { type: MarkType /** * npub of a counter part diff --git a/src/types/mark.ts b/src/types/mark.ts index efc1899..df733d6 100644 --- a/src/types/mark.ts +++ b/src/types/mark.ts @@ -18,10 +18,13 @@ export interface Mark { value?: string } -export interface MarkLocation { - top: number - left: number - height: number - width: number +export interface MarkLocation extends MarkRect { page: number } + +export interface MarkRect { + left: number + top: number + width: number + height: number +} diff --git a/src/utils/file.ts b/src/utils/file.ts index 26f03ef..63d40e5 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -22,11 +22,7 @@ export const getZipWithFiles = async ( for (const [fileName, file] of Object.entries(files)) { if (file.isPdf) { // Handle PDF Files - const pages = await addMarks( - file, - file.pages!, - marksByFileNamePage[fileName] - ) + const pages = await addMarks(file, marksByFileNamePage[fileName]) const blob = await convertToPdfBlob(pages) zip.file(`files/${fileName}`, blob) } else { @@ -78,6 +74,7 @@ export const getSigitFile = async (file: File) => { const sigitFile = new SigitFile(file) // Process sigit file // - generate pages for PDF files + // - generate ObjectRL for image files await sigitFile.process() return sigitFile } diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index c7eca36..15bcba7 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -8,11 +8,6 @@ PDFJS.GlobalWorkerOptions.workerSrc = new URL( import.meta.url ).toString() -/** - * Default width of the rendered element on the website - * @constant {number} - */ -export const DEFAULT_VIEWPORT_WIDTH = 550 /** * Scale between the PDF page's natural size and rendered size * @constant {number} @@ -32,11 +27,17 @@ export const FONT_SIZE: number = 16 export const FONT_TYPE: string = 'Arial' /** - * A utility that transforms a drawing coordinate number into a CSS-compatible string + * A utility that transforms a drawing coordinate number into a CSS-compatible pixel string * @param coordinate */ export const inPx = (coordinate: number): string => `${coordinate}px` +/** + * A utility that transforms a drawing coordinate number into a CSS-compatible percentage string + * @param coordinate + */ +export const inPerc = (coordinate: number): string => `${coordinate}%` + /** * A utility that checks if a given file is of the pdf type * @param file @@ -80,14 +81,14 @@ export const pdfToImages = async ( const pages: PdfPage[] = [] const pdf = await PDFJS.getDocument(data).promise const canvas = document.createElement('canvas') + const width = document.querySelector('#content-preview > *')?.clientWidth || 1 for (let i = 0; i < pdf.numPages; i++) { const page = await pdf.getPage(i + 1) const originalViewport = page.getViewport({ scale: 1 }) - const scale = originalViewport.width / DEFAULT_VIEWPORT_WIDTH - - const viewport = page.getViewport({ scale }) + const scale = width / originalViewport.width + const viewport = page.getViewport({ scale: scale }) const context = canvas.getContext('2d') canvas.height = viewport.height canvas.width = viewport.width @@ -110,7 +111,6 @@ export const pdfToImages = async ( */ export const addMarks = async ( file: File, - pages: PdfPage[], marksPerPage: { [key: string]: Mark[] } ) => { const p = await readPdf(file) @@ -129,7 +129,7 @@ export const addMarks = async ( await page.render({ canvasContext: context, viewport: viewport }).promise if (marksPerPage && Object.hasOwn(marksPerPage, i)) { - marksPerPage[i]?.forEach((mark) => draw(mark, context, pages[i].scale)) + marksPerPage[i]?.forEach((mark) => draw(mark, context)) } images.push(canvas.toDataURL()) @@ -169,14 +169,9 @@ export const hasValue = (mark: Mark): boolean => !!mark.value * @param mark to be drawn * @param ctx a Canvas representation of a specific PDF Page */ -export const draw = ( - mark: Mark, - ctx: CanvasRenderingContext2D, - scale: number -) => { - const { location } = scaleMark(mark, scale) - - ctx.font = scale * FONT_SIZE + 'px ' + FONT_TYPE +export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { + const { location } = mark + ctx.font = FONT_SIZE + 'px ' + FONT_TYPE ctx.fillStyle = 'black' const textMetrics = ctx.measureText(mark.value!) const textHeight = From 8c5be3713ddbcf49520323f259741e365be0cdc9 Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 26 Aug 2024 14:10:11 +0200 Subject: [PATCH 15/58] refactor(pdf): remove unused code --- src/utils/pdf.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index 15bcba7..8716ef5 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -8,11 +8,6 @@ PDFJS.GlobalWorkerOptions.workerSrc = new URL( import.meta.url ).toString() -/** - * Scale between the PDF page's natural size and rendered size - * @constant {number} - */ -export const DEFAULT_SCALE: number = 1 /** * 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 @@ -32,12 +27,6 @@ export const FONT_TYPE: string = 'Arial' */ export const inPx = (coordinate: number): string => `${coordinate}px` -/** - * A utility that transforms a drawing coordinate number into a CSS-compatible percentage string - * @param coordinate - */ -export const inPerc = (coordinate: number): string => `${coordinate}%` - /** * A utility that checks if a given file is of the pdf type * @param file From ec305c417bcca6d70a24cdae14c1b99be40b0064 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 27 Aug 2024 16:17:34 +0300 Subject: [PATCH 16/58] fix: signing order --- src/components/PDFView/PdfMarking.tsx | 6 +- src/pages/sign/index.tsx | 168 +++++++++++++------------- src/utils/sign.ts | 14 ++- 3 files changed, 100 insertions(+), 88 deletions(-) diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index 9fff924..be37fe7 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -24,7 +24,7 @@ interface PdfMarkingProps { meta: Meta | null otherUserMarks: Mark[] setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void - setIsReadyToSign: (isReadyToSign: boolean) => void + setIsMarksCompleted: (isMarksCompleted: boolean) => void setUpdatedMarks: (markToUpdate: Mark) => void } @@ -38,7 +38,7 @@ const PdfMarking = (props: PdfMarkingProps) => { const { files, currentUserMarks, - setIsReadyToSign, + setIsMarksCompleted, setCurrentUserMarks, setUpdatedMarks, handleDownload, @@ -102,7 +102,7 @@ const PdfMarking = (props: PdfMarkingProps) => { ) setCurrentUserMarks(updatedCurrentUserMarks) setSelectedMark(null) - setIsReadyToSign(true) + setIsMarksCompleted(true) setUpdatedMarks(updatedMark.mark) } diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index b8cfe5c..c7abf66 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -41,7 +41,7 @@ import styles from './style.module.scss' import { PdfFile } from '../../types/drawing.ts' import { convertToPdfFile } from '../../utils/pdf.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts' -import { getLastSignersSig } from '../../utils/sign.ts' +import { getLastSignersSig, isFullySigned } from '../../utils/sign.ts' import { filterMarksByPubkey, getCurrentUserMarks, @@ -110,13 +110,13 @@ export const SignPage = () => { const [currentUserMarks, setCurrentUserMarks] = useState( [] ) - const [isReadyToSign, setIsReadyToSign] = useState(false) + const [isMarksCompleted, setIsMarksCompleted] = useState(false) const [otherUserMarks, setOtherUserMarks] = useState([]) useEffect(() => { if (signers.length > 0) { // check if all signers have signed then its fully signed - if (signers.every((signer) => signedBy.includes(signer))) { + if (isFullySigned(signers, signedBy)) { setSignedStatus(SignedStatus.Fully_Signed) } else { for (const signer of signers) { @@ -214,7 +214,7 @@ export const SignPage = () => { const otherUserMarks = findOtherUserMarks(signedMarks, usersPubkey!) setOtherUserMarks(otherUserMarks) setCurrentUserMarks(currentUserMarks) - setIsReadyToSign(isCurrentUserMarksComplete(currentUserMarks)) + setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks)) } setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[]) @@ -882,90 +882,90 @@ export const SignPage = () => { return } - if (isReadyToSign) { + if (!isMarksCompleted && signedStatus === SignedStatus.User_Is_Next_Signer) { return ( - <> - - {displayInput && ( - <> - - Select sigit file - - - - setSelectedFile(value)} - /> - - - {selectedFile && ( - - - - )} - - )} - - {submittedBy && Object.entries(files).length > 0 && meta && ( - <> - - - {signedStatus === SignedStatus.Fully_Signed && ( - - - - )} - - {signedStatus === SignedStatus.User_Is_Next_Signer && ( - - - - )} - - {isSignerOrCreator && ( - - - - )} - - )} - - + ) } return ( - + <> + + {displayInput && ( + <> + + Select sigit file + + + + setSelectedFile(value)} + /> + + + {selectedFile && ( + + + + )} + + )} + + {submittedBy && Object.entries(files).length > 0 && meta && ( + <> + + + {signedStatus === SignedStatus.Fully_Signed && ( + + + + )} + + {signedStatus === SignedStatus.User_Is_Next_Signer && ( + + + + )} + + {isSignerOrCreator && ( + + + + )} + + )} + + ) } diff --git a/src/utils/sign.ts b/src/utils/sign.ts index 3369c54..ff67e44 100644 --- a/src/utils/sign.ts +++ b/src/utils/sign.ts @@ -31,4 +31,16 @@ const getLastSignersSig = ( } } -export { getLastSignersSig } +/** + * Checks if all signers have signed the sigit + * @param signers - an array of npubs of all signers from the Sigit + * @param signedBy - an array of npubs that have signed it already + */ +const isFullySigned = ( + signers: `npub1${string}`[], + signedBy: `npub1${string}`[] +): boolean => { + return signers.every((signer) => signedBy.includes(signer)) +} + +export { getLastSignersSig, isFullySigned } From 47120316152a7a3157f7cd089cb2184053ae25ec Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 27 Aug 2024 15:24:19 +0200 Subject: [PATCH 17/58] fix(pdf): scaling on resize, add avatars to counterpart select --- package-lock.json | 23 +++- package.json | 3 +- src/components/DrawPDFFields/index.tsx | 102 ++++++++++++++---- .../DrawPDFFields/style.module.scss | 2 +- src/components/PDFView/PdfMarkItem.tsx | 16 +-- src/components/PDFView/PdfPageItem.tsx | 15 +-- src/hooks/useScale.tsx | 52 +++++++++ src/pages/verify/index.tsx | 12 ++- src/types/drawing.ts | 2 +- src/utils/pdf.ts | 2 +- 10 files changed, 185 insertions(+), 44 deletions(-) create mode 100644 src/hooks/useScale.tsx diff --git a/package-lock.json b/package-lock.json index ef46577..479088b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,14 @@ { - "name": "web", + "name": "sigit", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "web", + "name": "sigit", "version": "0.0.0", "hasInstallScript": true, + "license": "AGPL-3.0-or-later ", "dependencies": { "@emotion/react": "11.11.4", "@emotion/styled": "11.11.0", @@ -40,6 +41,7 @@ "react-dropzone": "^14.2.3", "react-redux": "9.1.0", "react-router-dom": "6.22.1", + "react-singleton-hook": "^4.0.1", "react-toastify": "10.0.4", "redux": "5.0.1", "tseep": "1.2.1" @@ -5832,6 +5834,23 @@ "react-dom": ">=16.8" } }, + "node_modules/react-singleton-hook": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/react-singleton-hook/-/react-singleton-hook-4.0.1.tgz", + "integrity": "sha512-fWuk8VxcZPChrkQasDLM8pgd/7kyi+Cr/5FfCiD99FicjEru+JmtEZNnN4lJ8Z7KbqAST5CYPlpz6lmNsZFGNw==", + "license": "MIT", + "peerDependencies": { + "react": "18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-toastify": { "version": "10.0.4", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.4.tgz", diff --git a/package.json b/package.json index c835018..a2f074d 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "react-dropzone": "^14.2.3", "react-redux": "9.1.0", "react-router-dom": "6.22.1", + "react-singleton-hook": "^4.0.1", "react-toastify": "10.0.4", "redux": "5.0.1", "tseep": "1.2.1" @@ -82,4 +83,4 @@ ], "*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}": "npm run formatter:staged" } -} \ No newline at end of file +} diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 35de444..b808c40 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -4,6 +4,8 @@ import { CircularProgress, FormControl, InputLabel, + ListItemIcon, + ListItemText, MenuItem, Select } from '@mui/material' @@ -13,11 +15,13 @@ import * as PDFJS from 'pdfjs-dist' import { ProfileMetadata, User, UserRole } from '../../types' import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing' import { truncate } from 'lodash' -import { settleAllFullfilfedPromises, hexToNpub } from '../../utils' +import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils' import { getSigitFile, SigitFile } from '../../utils/file' import { FileDivider } from '../FileDivider' import { ExtensionFileBox } from '../ExtensionFileBox' import { inPx } from '../../utils/pdf' +import { useScale } from '../../hooks/useScale' +import { AvatarIconButton } from '../UserAvatarIconButton' PDFJS.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -34,6 +38,7 @@ interface Props { export const DrawPDFFields = (props: Props) => { const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props + const { to, from } = useScale() const [sigitFiles, setSigitFiles] = useState([]) const [parsingPdf, setIsParsing] = useState(false) @@ -106,8 +111,8 @@ export const DrawPDFFields = (props: Props) => { const { mouseX, mouseY } = getMouseCoordinates(event) const newField: DrawnField = { - left: mouseX / page.scale, - top: mouseY / page.scale, + left: to(page.width, mouseX), + top: to(page.width, mouseY), width: 0, height: 0, counterpart: '', @@ -161,8 +166,8 @@ export const DrawPDFFields = (props: Props) => { const { mouseX, mouseY } = getMouseCoordinates(event) - const width = mouseX / page.scale - lastDrawnField.left - const height = mouseY / page.scale - lastDrawnField.top + const width = to(page.width, mouseX) - lastDrawnField.left + const height = to(page.width, mouseY) - lastDrawnField.top lastDrawnField.width = width lastDrawnField.height = height @@ -211,7 +216,7 @@ export const DrawPDFFields = (props: Props) => { const onDrawnFieldMouseMove = ( event: React.MouseEvent, drawnField: DrawnField, - scale: number + pageWidth: number ) => { if (mouseState.dragging) { const { mouseX, mouseY, rect } = getMouseCoordinates( @@ -221,11 +226,11 @@ export const DrawPDFFields = (props: Props) => { const coordsOffset = mouseState.coordsInWrapper if (coordsOffset) { - let left = (mouseX - coordsOffset.mouseX) / scale - let top = (mouseY - coordsOffset.mouseY) / scale + let left = to(pageWidth, mouseX - coordsOffset.mouseX) + let top = to(pageWidth, mouseY - coordsOffset.mouseY) - const rightLimit = rect.width / scale - drawnField.width - 3 - const bottomLimit = rect.height / scale - drawnField.height - 3 + const rightLimit = to(pageWidth, rect.width) - drawnField.width - 3 + const bottomLimit = to(pageWidth, rect.height) - drawnField.height - 3 if (left < 0) left = 0 if (top < 0) top = 0 @@ -266,7 +271,7 @@ export const DrawPDFFields = (props: Props) => { const onResizeHandleMouseMove = ( event: React.MouseEvent, drawnField: DrawnField, - scale: number + pageWidth: number ) => { if (mouseState.resizing) { const { mouseX, mouseY } = getMouseCoordinates( @@ -277,8 +282,8 @@ export const DrawPDFFields = (props: Props) => { event.currentTarget.parentElement?.parentElement ) - const width = mouseX / scale - drawnField.left - const height = mouseY / scale - drawnField.top + const width = to(pageWidth, mouseX) - drawnField.left + const height = to(pageWidth, mouseY) - drawnField.top drawnField.width = width drawnField.height = height @@ -375,21 +380,21 @@ export const DrawPDFFields = (props: Props) => { key={drawnFieldIndex} onMouseDown={onDrawnFieldMouseDown} onMouseMove={(event) => { - onDrawnFieldMouseMove(event, drawnField, page.scale) + onDrawnFieldMouseMove(event, drawnField, page.width) }} className={styles.drawingRectangle} style={{ - left: inPx(drawnField.left * page.scale), - top: inPx(drawnField.top * page.scale), - width: inPx(drawnField.width * page.scale), - height: inPx(drawnField.height * page.scale), + left: inPx(from(page.width, drawnField.left)), + top: inPx(from(page.width, drawnField.top)), + width: inPx(from(page.width, drawnField.width)), + height: inPx(from(page.width, drawnField.height)), pointerEvents: mouseState.clicked ? 'none' : 'all' }} > { - onResizeHandleMouseMove(event, drawnField, page.scale) + onResizeHandleMouseMove(event, drawnField, page.width) }} className={styles.resizeHandle} > @@ -423,6 +428,9 @@ export const DrawPDFFields = (props: Props) => { sx={{ background: 'white' }} + renderValue={(value) => + renderCounterpartValue(drawnField, value) + } > {users .filter((u) => u.role === UserRole.signer) @@ -451,7 +459,22 @@ export const DrawPDFFields = (props: Props) => { key={index} value={hexToNpub(user.pubkey)} > - {displayValue} + + img': { + width: '30px', + height: '30px' + } + }} + /> + + {displayValue} ) })} @@ -468,6 +491,45 @@ export const DrawPDFFields = (props: Props) => { ) } + const renderCounterpartValue = (drawnField: DrawnField, value: string) => { + const user = users.find((u) => u.pubkey === npubToHex(value)) + if (user) { + let displayValue = truncate(value, { + length: 16 + }) + + const metadata = props.metadata[value] + + if (metadata) { + displayValue = truncate( + metadata.name || metadata.display_name || metadata.username || value, + { + length: 16 + } + ) + } + return ( + <> + img': { + width: '21px', + height: '21px' + } + }} + /> + {displayValue} + + ) + } + + return value + } + if (parsingPdf) { return ( diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index b3150b3..62fa688 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -73,7 +73,7 @@ justify-content: center; align-items: center; bottom: -60px; - min-width: 170px; + min-width: 193px; min-height: 30px; padding: 5px 0; } diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx index cc9c9b2..d5a7c78 100644 --- a/src/components/PDFView/PdfMarkItem.tsx +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -1,13 +1,14 @@ import { CurrentUserMark } from '../../types/mark.ts' import styles from '../DrawPDFFields/style.module.scss' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts' +import { useScale } from '../../hooks/useScale.tsx' interface PdfMarkItemProps { userMark: CurrentUserMark handleMarkClick: (id: number) => void selectedMarkValue: string selectedMark: CurrentUserMark | null - scale: number + pageWidth: number } /** @@ -18,24 +19,25 @@ const PdfMarkItem = ({ handleMarkClick, selectedMarkValue, userMark, - scale + pageWidth }: PdfMarkItemProps) => { const { location } = userMark.mark const handleClick = () => handleMarkClick(userMark.mark.id) const isEdited = () => selectedMark?.mark.id === userMark.mark.id const getMarkValue = () => isEdited() ? selectedMarkValue : userMark.currentValue + const { from } = useScale() return (
{getMarkValue()} diff --git a/src/components/PDFView/PdfPageItem.tsx b/src/components/PDFView/PdfPageItem.tsx index f5f310d..518f06d 100644 --- a/src/components/PDFView/PdfPageItem.tsx +++ b/src/components/PDFView/PdfPageItem.tsx @@ -5,6 +5,7 @@ import PdfMarkItem from './PdfMarkItem.tsx' import { useEffect, useRef } from 'react' import pdfViewStyles from './style.module.scss' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts' +import { useScale } from '../../hooks/useScale.tsx' interface PdfPageProps { currentUserMarks: CurrentUserMark[] handleMarkClick: (id: number) => void @@ -33,6 +34,8 @@ const PdfPageItem = ({ } }, [selectedMark]) const markRefs = useRef<(HTMLDivElement | null)[]>([]) + const { from } = useScale() + return (
@@ -44,7 +47,7 @@ const PdfPageItem = ({ selectedMarkValue={selectedMarkValue} userMark={m} selectedMark={selectedMark} - scale={page.scale} + pageWidth={page.width} />
))} @@ -54,12 +57,12 @@ const PdfPageItem = ({ key={i} className={pdfViewStyles.otherUserMarksDisplay} style={{ - left: inPx(m.location.left * page.scale), - top: inPx(m.location.top * page.scale), - width: inPx(m.location.width * page.scale), - height: inPx(m.location.height * page.scale), + left: inPx(from(page.width, m.location.left)), + top: inPx(from(page.width, m.location.top)), + width: inPx(from(page.width, m.location.width)), + height: inPx(from(page.width, m.location.height)), fontFamily: FONT_TYPE, - fontSize: inPx(FONT_SIZE * page.scale) + fontSize: inPx(from(page.width, FONT_SIZE)) }} > {m.value} diff --git a/src/hooks/useScale.tsx b/src/hooks/useScale.tsx new file mode 100644 index 0000000..89eb8a5 --- /dev/null +++ b/src/hooks/useScale.tsx @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react' +import { singletonHook } from 'react-singleton-hook' + +const noScaleInit = { + to: (_: number, v: number) => v, + from: (_: number, v: number) => v +} + +const useScaleImpl = () => { + const [width, setWidth] = useState( + document.querySelector('#content-preview > *')?.clientWidth || 1 + ) + + // Get the scale based on the original width + const scale = (originalWidth: number) => { + return width / originalWidth + } + + // Get the original pixel value + const to = (originalWidth: number, value: number) => { + return value / scale(originalWidth) + } + + // Get the scaled pixel value + const from = (originalWidth: number, value: number) => { + return value * scale(originalWidth) + } + + useEffect(() => { + const resize = () => { + // Fetch the first container element we find + const element = document.querySelector('#content-preview > *') + + // Set the width state + if (element) { + setWidth(element.clientWidth) + } + } + resize() + + window.addEventListener('resize', resize) + return () => { + window.removeEventListener('resize', resize) + } + }, []) + + return { to, from } +} + +export const useScale = singletonHook(noScaleInit, useScaleImpl, { + unmountIfNoConsumers: true +}) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 6ff679a..8619806 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -52,6 +52,7 @@ import React from 'react' import { convertToSigitFile, SigitFile } from '../../utils/file.ts' import { FileDivider } from '../../components/FileDivider.tsx' import { ExtensionFileBox } from '../../components/ExtensionFileBox.tsx' +import { useScale } from '../../hooks/useScale.tsx' interface PdfViewProps { files: CurrentUserFile[] @@ -67,6 +68,7 @@ const SlimPdfView = ({ parsedSignatureEvents }: PdfViewProps) => { const pdfRefs = useRef<(HTMLDivElement | null)[]>([]) + const { from } = useScale() useEffect(() => { if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { pdfRefs.current[currentFile.id]?.scrollIntoView({ @@ -110,12 +112,12 @@ const SlimPdfView = ({ className={`file-mark ${styles.mark}`} key={m.id} style={{ - left: inPx(m.location.left * page.scale), - top: inPx(m.location.top * page.scale), - width: inPx(m.location.width * page.scale), - height: inPx(m.location.height * page.scale), + left: inPx(from(page.width, m.location.left)), + top: inPx(from(page.width, m.location.top)), + width: inPx(from(page.width, m.location.width)), + height: inPx(from(page.width, m.location.height)), fontFamily: FONT_TYPE, - fontSize: inPx(FONT_SIZE * page.scale) + fontSize: inPx(from(page.width, FONT_SIZE)) }} > {m.value} diff --git a/src/types/drawing.ts b/src/types/drawing.ts index d8c5cc3..677b68c 100644 --- a/src/types/drawing.ts +++ b/src/types/drawing.ts @@ -12,7 +12,7 @@ export interface MouseState { export interface PdfPage { image: string - scale: number + width: number drawnFields: DrawnField[] } diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index 8716ef5..b1e9847 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -85,7 +85,7 @@ export const pdfToImages = async ( await page.render({ canvasContext: context!, viewport: viewport }).promise pages.push({ image: canvas.toDataURL(), - scale, + width: originalViewport.width, drawnFields: [] }) } From 923a47b4d086563fc1f514b962cfdecb281d6b87 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 27 Aug 2024 16:10:57 +0200 Subject: [PATCH 18/58] fix(drawfield): match label and select --- src/components/DrawPDFFields/index.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index b808c40..66f6952 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -428,9 +428,7 @@ export const DrawPDFFields = (props: Props) => { sx={{ background: 'white' }} - renderValue={(value) => - renderCounterpartValue(drawnField, value) - } + renderValue={(value) => renderCounterpartValue(value)} > {users .filter((u) => u.role === UserRole.signer) @@ -491,14 +489,14 @@ export const DrawPDFFields = (props: Props) => { ) } - const renderCounterpartValue = (drawnField: DrawnField, value: string) => { + const renderCounterpartValue = (value: string) => { const user = users.find((u) => u.pubkey === npubToHex(value)) if (user) { let displayValue = truncate(value, { length: 16 }) - const metadata = props.metadata[value] + const metadata = props.metadata[user.pubkey] if (metadata) { displayValue = truncate( @@ -511,8 +509,8 @@ export const DrawPDFFields = (props: Props) => { return ( <> Date: Tue, 27 Aug 2024 17:28:18 +0200 Subject: [PATCH 19/58] fix(pdf): add proper default width value --- src/hooks/useScale.tsx | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/hooks/useScale.tsx b/src/hooks/useScale.tsx index 89eb8a5..86de61f 100644 --- a/src/hooks/useScale.tsx +++ b/src/hooks/useScale.tsx @@ -6,10 +6,26 @@ const noScaleInit = { from: (_: number, v: number) => v } +const getInnerContentWidth = () => { + // Fetch the first container element we find + const element = document.querySelector('#content-preview') + + if (element) { + const style = getComputedStyle(element) + + // Calculate width without padding + const widthWithoutPadding = + element.clientWidth - parseFloat(style.padding) * 2 + + return widthWithoutPadding + } + + // Default value + return 620 +} + const useScaleImpl = () => { - const [width, setWidth] = useState( - document.querySelector('#content-preview > *')?.clientWidth || 1 - ) + const [width, setWidth] = useState(getInnerContentWidth()) // Get the scale based on the original width const scale = (originalWidth: number) => { @@ -26,16 +42,11 @@ const useScaleImpl = () => { return value * scale(originalWidth) } - useEffect(() => { - const resize = () => { - // Fetch the first container element we find - const element = document.querySelector('#content-preview > *') + const resize = () => { + setWidth(getInnerContentWidth()) + } - // Set the width state - if (element) { - setWidth(element.clientWidth) - } - } + useEffect(() => { resize() window.addEventListener('resize', resize) From 59c3fc69a255c54475daa6a2efad3ae8a4b3efd8 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 28 Aug 2024 11:41:29 +0200 Subject: [PATCH 20/58] fix(pdf): reuse content width function --- src/hooks/useScale.tsx | 19 +------------------ src/utils/pdf.ts | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/hooks/useScale.tsx b/src/hooks/useScale.tsx index 86de61f..406928b 100644 --- a/src/hooks/useScale.tsx +++ b/src/hooks/useScale.tsx @@ -1,29 +1,12 @@ import { useEffect, useState } from 'react' import { singletonHook } from 'react-singleton-hook' +import { getInnerContentWidth } from '../utils/pdf' const noScaleInit = { to: (_: number, v: number) => v, from: (_: number, v: number) => v } -const getInnerContentWidth = () => { - // Fetch the first container element we find - const element = document.querySelector('#content-preview') - - if (element) { - const style = getComputedStyle(element) - - // Calculate width without padding - const widthWithoutPadding = - element.clientWidth - parseFloat(style.padding) * 2 - - return widthWithoutPadding - } - - // Default value - return 620 -} - const useScaleImpl = () => { const [width, setWidth] = useState(getInnerContentWidth()) diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index b1e9847..1dd75f6 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -60,6 +60,24 @@ export const readPdf = (file: File): Promise => { }) } +export const getInnerContentWidth = () => { + // Fetch the first container element we find + const element = document.querySelector('#content-preview') + + if (element) { + const style = getComputedStyle(element) + + // Calculate width without padding + const widthWithoutPadding = + element.clientWidth - parseFloat(style.padding) * 2 + + return widthWithoutPadding + } + + // Default value + return 620 +} + /** * Converts pdf to the images * @param data pdf file bytes @@ -70,7 +88,7 @@ export const pdfToImages = async ( const pages: PdfPage[] = [] const pdf = await PDFJS.getDocument(data).promise const canvas = document.createElement('canvas') - const width = document.querySelector('#content-preview > *')?.clientWidth || 1 + const width = getInnerContentWidth() for (let i = 0; i < pdf.numPages; i++) { const page = await pdf.getPage(i + 1) From 3a03e3c7e84aa74b02f355e4fcbe273ed06bcd47 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 27 Aug 2024 17:47:19 +0200 Subject: [PATCH 21/58] chore: comment typo --- src/utils/mark.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/mark.ts b/src/utils/mark.ts index effa2ba..44540c4 100644 --- a/src/utils/mark.ts +++ b/src/utils/mark.ts @@ -47,7 +47,7 @@ const filterMarksByPubkey = (marks: Mark[], pubkey: string): Mark[] => { /** * Takes Signed Doc Signatures part of Meta and extracts - * all Marks into one flar array, regardless of the user. + * all Marks into one flat array, regardless of the user. * @param meta */ const extractMarksFromSignedMeta = (meta: Meta): Mark[] => { From 624afae8514420a36034ef29814920579da308f6 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 27 Aug 2024 17:58:07 +0200 Subject: [PATCH 22/58] fix(create): throw on mark with no counterpart --- src/pages/create/index.tsx | 272 ++++++++++++++++++++----------------- 1 file changed, 148 insertions(+), 124 deletions(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 2bcd063..d5f5355 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -514,6 +514,9 @@ export const CreatePage = () => { return ( file.pages?.flatMap((page, index) => { return page.drawnFields.map((drawnField) => { + if (!drawnField.counterpart) { + throw new Error('Missing counterpart') + } return { type: drawnField.type, location: { @@ -670,6 +673,7 @@ export const CreatePage = () => { } const generateCreateSignature = async ( + markConfig: Mark[], fileHashes: { [key: string]: string }, @@ -677,7 +681,6 @@ export const CreatePage = () => { ) => { const signers = users.filter((user) => user.role === UserRole.signer) const viewers = users.filter((user) => user.role === UserRole.viewer) - const markConfig = createMarks(fileHashes) const content: CreateSignatureEventContent = { signers: signers.map((signer) => hexToNpub(signer.pubkey)), @@ -721,131 +724,152 @@ export const CreatePage = () => { } const handleCreate = async () => { - if (!validateInputs()) return + try { + if (!validateInputs()) return - setIsLoading(true) - setLoadingSpinnerDesc('Generating file hashes') - const fileHashes = await generateFileHashes() - if (!fileHashes) { + setIsLoading(true) + setLoadingSpinnerDesc('Generating file hashes') + const fileHashes = await generateFileHashes() + if (!fileHashes) { + setIsLoading(false) + return + } + + setLoadingSpinnerDesc('Generating encryption key') + const encryptionKey = await generateEncryptionKey() + + if (await isOnline()) { + setLoadingSpinnerDesc('generating files.zip') + const arrayBuffer = await generateFilesZip() + if (!arrayBuffer) { + setIsLoading(false) + return + } + + setLoadingSpinnerDesc('Encrypting files.zip') + const encryptedArrayBuffer = await encryptZipFile( + arrayBuffer, + encryptionKey + ) + + const markConfig = createMarks(fileHashes) + + setLoadingSpinnerDesc('Uploading files.zip to file storage') + const fileUrl = await uploadFile(encryptedArrayBuffer) + if (!fileUrl) { + setIsLoading(false) + return + } + + setLoadingSpinnerDesc('Generating create signature') + const createSignature = await generateCreateSignature( + markConfig, + fileHashes, + fileUrl + ) + if (!createSignature) { + setIsLoading(false) + return + } + + setLoadingSpinnerDesc('Generating keys for decryption') + + // generate key pairs for decryption + const pubkeys = users.map((user) => user.pubkey) + // also add creator in the list + if (pubkeys.includes(usersPubkey!)) { + pubkeys.push(usersPubkey!) + } + + const keys = await generateKeys(pubkeys, encryptionKey) + + if (!keys) { + setIsLoading(false) + return + } + const meta: Meta = { + createSignature, + keys, + modifiedAt: unixNow(), + docSignatures: {} + } + + setLoadingSpinnerDesc('Updating user app data') + const event = await updateUsersAppData(meta) + if (!event) { + setIsLoading(false) + return + } + + setLoadingSpinnerDesc('Sending notifications to counterparties') + const promises = sendNotifications(meta) + + await Promise.all(promises) + .then(() => { + toast.success('Notifications sent successfully') + }) + .catch(() => { + toast.error('Failed to publish notifications') + }) + + navigate(appPrivateRoutes.sign, { state: { meta: meta } }) + } else { + const zip = new JSZip() + + selectedFiles.forEach((file) => { + zip.file(`files/${file.name}`, file) + }) + + const markConfig = createMarks(fileHashes) + + setLoadingSpinnerDesc('Generating create signature') + const createSignature = await generateCreateSignature( + markConfig, + fileHashes, + '' + ) + if (!createSignature) { + setIsLoading(false) + return + } + + const meta: Meta = { + createSignature, + modifiedAt: unixNow(), + docSignatures: {} + } + + // add meta to zip + try { + const stringifiedMeta = JSON.stringify(meta, null, 2) + zip.file('meta.json', stringifiedMeta) + } catch (err) { + console.error(err) + toast.error('An error occurred in converting meta json to string') + return null + } + + const arrayBuffer = await generateZipFile(zip) + if (!arrayBuffer) { + setIsLoading(false) + return + } + + setLoadingSpinnerDesc('Encrypting zip file') + const encryptedArrayBuffer = await encryptZipFile( + arrayBuffer, + encryptionKey + ) + + await handleOfflineFlow(encryptedArrayBuffer, encryptionKey) + } + } catch (error) { + if (error instanceof Error) { + toast.error(error.message) + } + console.error(error) + } finally { setIsLoading(false) - return - } - - setLoadingSpinnerDesc('Generating encryption key') - const encryptionKey = await generateEncryptionKey() - - if (await isOnline()) { - setLoadingSpinnerDesc('generating files.zip') - const arrayBuffer = await generateFilesZip() - if (!arrayBuffer) { - setIsLoading(false) - return - } - - setLoadingSpinnerDesc('Encrypting files.zip') - const encryptedArrayBuffer = await encryptZipFile( - arrayBuffer, - encryptionKey - ) - - setLoadingSpinnerDesc('Uploading files.zip to file storage') - const fileUrl = await uploadFile(encryptedArrayBuffer) - if (!fileUrl) { - setIsLoading(false) - return - } - - setLoadingSpinnerDesc('Generating create signature') - const createSignature = await generateCreateSignature(fileHashes, fileUrl) - if (!createSignature) { - setIsLoading(false) - return - } - - setLoadingSpinnerDesc('Generating keys for decryption') - - // generate key pairs for decryption - const pubkeys = users.map((user) => user.pubkey) - // also add creator in the list - if (pubkeys.includes(usersPubkey!)) { - pubkeys.push(usersPubkey!) - } - - const keys = await generateKeys(pubkeys, encryptionKey) - - if (!keys) { - setIsLoading(false) - return - } - const meta: Meta = { - createSignature, - keys, - modifiedAt: unixNow(), - docSignatures: {} - } - - setLoadingSpinnerDesc('Updating user app data') - const event = await updateUsersAppData(meta) - if (!event) { - setIsLoading(false) - return - } - - setLoadingSpinnerDesc('Sending notifications to counterparties') - const promises = sendNotifications(meta) - - await Promise.all(promises) - .then(() => { - toast.success('Notifications sent successfully') - }) - .catch(() => { - toast.error('Failed to publish notifications') - }) - - navigate(appPrivateRoutes.sign, { state: { meta: meta } }) - } else { - const zip = new JSZip() - - selectedFiles.forEach((file) => { - zip.file(`files/${file.name}`, file) - }) - - setLoadingSpinnerDesc('Generating create signature') - const createSignature = await generateCreateSignature(fileHashes, '') - if (!createSignature) { - setIsLoading(false) - return - } - - const meta: Meta = { - createSignature, - modifiedAt: unixNow(), - docSignatures: {} - } - - // add meta to zip - try { - const stringifiedMeta = JSON.stringify(meta, null, 2) - zip.file('meta.json', stringifiedMeta) - } catch (err) { - console.error(err) - toast.error('An error occurred in converting meta json to string') - return null - } - - const arrayBuffer = await generateZipFile(zip) - if (!arrayBuffer) { - setIsLoading(false) - return - } - - setLoadingSpinnerDesc('Encrypting zip file') - const encryptedArrayBuffer = await encryptZipFile( - arrayBuffer, - encryptionKey - ) - - await handleOfflineFlow(encryptedArrayBuffer, encryptionKey) } } From fa1811a330d510cffa87ce497a1abe2384359a69 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 28 Aug 2024 09:29:05 +0200 Subject: [PATCH 23/58] refactor(error): refactor SigitMetaParseError --- src/hooks/useSigitMeta.tsx | 4 +-- src/types/errors/MetaParseError.ts | 17 ++++++++++ src/types/errors/index.ts | 29 ++++++++++++++++ src/utils/meta.ts | 54 +++--------------------------- 4 files changed, 53 insertions(+), 51 deletions(-) create mode 100644 src/types/errors/MetaParseError.ts create mode 100644 src/types/errors/index.ts diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index fea5154..088940e 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -11,7 +11,6 @@ import { hexToNpub, parseNostrEvent, parseCreateSignatureEventContent, - SigitMetaParseError, SigitStatus, SignStatus } from '../utils' @@ -21,6 +20,7 @@ import { Event } from 'nostr-tools' import store from '../store/store' import { AuthState } from '../store/auth/types' import { NostrController } from '../controllers' +import { MetaParseError } from '../types/errors/MetaParseError' /** * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`, @@ -247,7 +247,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { ) } } catch (error) { - if (error instanceof SigitMetaParseError) { + if (error instanceof MetaParseError) { toast.error(error.message) } console.error(error) diff --git a/src/types/errors/MetaParseError.ts b/src/types/errors/MetaParseError.ts new file mode 100644 index 0000000..85cddec --- /dev/null +++ b/src/types/errors/MetaParseError.ts @@ -0,0 +1,17 @@ +import { Jsonable } from '.' + +export class MetaParseError extends Error { + public readonly context?: Jsonable + + constructor( + message: string, + options: { cause?: Error; context?: Jsonable } = {} + ) { + const { cause, context } = options + + super(message, { cause }) + this.name = this.constructor.name + + this.context = context + } +} diff --git a/src/types/errors/index.ts b/src/types/errors/index.ts new file mode 100644 index 0000000..6ef8990 --- /dev/null +++ b/src/types/errors/index.ts @@ -0,0 +1,29 @@ +export type Jsonable = + | string + | number + | boolean + | null + | undefined + | readonly Jsonable[] + | { readonly [key: string]: Jsonable } + | { toJSON(): Jsonable } + +/** + * Handle errors + * Wraps the errors without message property and stringify to a message so we can use it later + * @param error + * @returns + */ +export function handleError(error: unknown): Error { + if (error instanceof Error) return error + + // No message error, wrap it and stringify + let stringified = 'Unable to stringify the thrown value' + try { + stringified = JSON.stringify(error) + } catch (error) { + console.error(stringified, error) + } + + return new Error(`Wrapped Error: ${stringified}`) +} diff --git a/src/utils/meta.ts b/src/utils/meta.ts index fd3481c..62eb740 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -3,6 +3,8 @@ import { fromUnixTimestamp, parseJson } from '.' import { Event, verifyEvent } from 'nostr-tools' import { toast } from 'react-toastify' import { extractFileExtensions } from './file' +import { handleError } from '../types/errors' +import { MetaParseError } from '../types/errors/MetaParseError' export enum SignStatus { Signed = 'Signed', @@ -17,52 +19,6 @@ export enum SigitStatus { Complete = 'Completed' } -type Jsonable = - | string - | number - | boolean - | null - | undefined - | readonly Jsonable[] - | { readonly [key: string]: Jsonable } - | { toJSON(): Jsonable } - -export class SigitMetaParseError extends Error { - public readonly context?: Jsonable - - constructor( - message: string, - options: { cause?: Error; context?: Jsonable } = {} - ) { - const { cause, context } = options - - super(message, { cause }) - this.name = this.constructor.name - - this.context = context - } -} - -/** - * Handle meta errors - * Wraps the errors without message property and stringify to a message so we can use it later - * @param error - * @returns - */ -function handleError(error: unknown): Error { - if (error instanceof Error) return error - - // No message error, wrap it and stringify - let stringified = 'Unable to stringify the thrown value' - try { - stringified = JSON.stringify(error) - } catch (error) { - console.error(stringified, error) - } - - return new Error(`[SiGit Error]: ${stringified}`) -} - // Reuse common error messages for meta parsing export enum SigitMetaParseErrorType { 'PARSE_ERROR_EVENT' = 'error occurred in parsing the create signature event', @@ -89,7 +45,7 @@ export const parseNostrEvent = async (raw: string): Promise => { const event = await parseJson(raw) return event } catch (error) { - throw new SigitMetaParseError(SigitMetaParseErrorType.PARSE_ERROR_EVENT, { + throw new MetaParseError(SigitMetaParseErrorType.PARSE_ERROR_EVENT, { cause: handleError(error), context: raw }) @@ -109,7 +65,7 @@ export const parseCreateSignatureEventContent = async ( await parseJson(raw) return createSignatureEventContent } catch (error) { - throw new SigitMetaParseError( + throw new MetaParseError( SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT, { cause: handleError(error), @@ -165,7 +121,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { return sigitInfo } catch (error) { - if (error instanceof SigitMetaParseError) { + if (error instanceof MetaParseError) { toast.error(error.message) console.error(error.name, error.message, error.cause, error.context) } else { From 1c998ab99feb780f6183bcb7bd518eb3f2d77736 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 28 Aug 2024 09:32:23 +0200 Subject: [PATCH 24/58] refactor(error): refactor MetaParseError 2 --- src/types/errors/MetaParseError.ts | 6 ++++++ src/utils/meta.ts | 15 ++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/types/errors/MetaParseError.ts b/src/types/errors/MetaParseError.ts index 85cddec..96adab5 100644 --- a/src/types/errors/MetaParseError.ts +++ b/src/types/errors/MetaParseError.ts @@ -1,5 +1,11 @@ import { Jsonable } from '.' +// Reuse common error messages for meta parsing +export enum MetaParseErrorType { + 'PARSE_ERROR_EVENT' = 'error occurred in parsing the create signature event', + 'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content" +} + export class MetaParseError extends Error { public readonly context?: Jsonable diff --git a/src/utils/meta.ts b/src/utils/meta.ts index 62eb740..c915f66 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -4,7 +4,10 @@ import { Event, verifyEvent } from 'nostr-tools' import { toast } from 'react-toastify' import { extractFileExtensions } from './file' import { handleError } from '../types/errors' -import { MetaParseError } from '../types/errors/MetaParseError' +import { + MetaParseError, + MetaParseErrorType +} from '../types/errors/MetaParseError' export enum SignStatus { Signed = 'Signed', @@ -19,12 +22,6 @@ export enum SigitStatus { Complete = 'Completed' } -// Reuse common error messages for meta parsing -export enum SigitMetaParseErrorType { - 'PARSE_ERROR_EVENT' = 'error occurred in parsing the create signature event', - 'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content" -} - export interface SigitCardDisplayInfo { createdAt?: number title?: string @@ -45,7 +42,7 @@ export const parseNostrEvent = async (raw: string): Promise => { const event = await parseJson(raw) return event } catch (error) { - throw new MetaParseError(SigitMetaParseErrorType.PARSE_ERROR_EVENT, { + throw new MetaParseError(MetaParseErrorType.PARSE_ERROR_EVENT, { cause: handleError(error), context: raw }) @@ -66,7 +63,7 @@ export const parseCreateSignatureEventContent = async ( return createSignatureEventContent } catch (error) { throw new MetaParseError( - SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT, + MetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT, { cause: handleError(error), context: raw From a8020e6db2ad88f29e6cf2bb35cca1cace56cb07 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 28 Aug 2024 09:45:15 +0200 Subject: [PATCH 25/58] fix(column-layout): wrap content column to prevent expanding --- src/layouts/StickySideColumns.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/layouts/StickySideColumns.tsx b/src/layouts/StickySideColumns.tsx index 43dc430..d27ad4b 100644 --- a/src/layouts/StickySideColumns.tsx +++ b/src/layouts/StickySideColumns.tsx @@ -17,8 +17,10 @@ export const StickySideColumns = ({
{left}
-
- {children} +
+
+ {children} +
{right}
From 20d1170f7dd41832b83b34656a9f95e239f074cf Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 28 Aug 2024 09:48:15 +0200 Subject: [PATCH 26/58] fix(sign): allow signing without marks, hide loading and show toast for prevSig error --- src/pages/sign/index.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 7ca88e6..8720954 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -542,10 +542,13 @@ export const SignPage = () => { setLoadingSpinnerDesc('Signing nostr event') const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!)) - if (!prevSig) return + if (!prevSig) { + setIsLoading(false) + toast.error('Previous signature is invalid') + return + } - const marks = getSignerMarksForMeta() - if (!marks) return + const marks = getSignerMarksForMeta() || [] const signedEvent = await signEventForMeta({ prevSig, marks }) if (!signedEvent) return From e2b12afc721e7e809c5220e42c3db57d9ab86918 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 29 Aug 2024 12:54:51 +0200 Subject: [PATCH 27/58] refactor(review): handle loading state, browser warnings --- src/pages/create/index.tsx | 82 ++++++++++++++------------------------ 1 file changed, 29 insertions(+), 53 deletions(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index d5f5355..2c32f7d 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -730,10 +730,7 @@ export const CreatePage = () => { setIsLoading(true) setLoadingSpinnerDesc('Generating file hashes') const fileHashes = await generateFileHashes() - if (!fileHashes) { - setIsLoading(false) - return - } + if (!fileHashes) return setLoadingSpinnerDesc('Generating encryption key') const encryptionKey = await generateEncryptionKey() @@ -741,10 +738,7 @@ export const CreatePage = () => { if (await isOnline()) { setLoadingSpinnerDesc('generating files.zip') const arrayBuffer = await generateFilesZip() - if (!arrayBuffer) { - setIsLoading(false) - return - } + if (!arrayBuffer) return setLoadingSpinnerDesc('Encrypting files.zip') const encryptedArrayBuffer = await encryptZipFile( @@ -756,10 +750,7 @@ export const CreatePage = () => { setLoadingSpinnerDesc('Uploading files.zip to file storage') const fileUrl = await uploadFile(encryptedArrayBuffer) - if (!fileUrl) { - setIsLoading(false) - return - } + if (!fileUrl) return setLoadingSpinnerDesc('Generating create signature') const createSignature = await generateCreateSignature( @@ -767,10 +758,7 @@ export const CreatePage = () => { fileHashes, fileUrl ) - if (!createSignature) { - setIsLoading(false) - return - } + if (!createSignature) return setLoadingSpinnerDesc('Generating keys for decryption') @@ -782,11 +770,8 @@ export const CreatePage = () => { } const keys = await generateKeys(pubkeys, encryptionKey) + if (!keys) return - if (!keys) { - setIsLoading(false) - return - } const meta: Meta = { createSignature, keys, @@ -796,10 +781,7 @@ export const CreatePage = () => { setLoadingSpinnerDesc('Updating user app data') const event = await updateUsersAppData(meta) - if (!event) { - setIsLoading(false) - return - } + if (!event) return setLoadingSpinnerDesc('Sending notifications to counterparties') const promises = sendNotifications(meta) @@ -828,10 +810,7 @@ export const CreatePage = () => { fileHashes, '' ) - if (!createSignature) { - setIsLoading(false) - return - } + if (!createSignature) return const meta: Meta = { createSignature, @@ -850,10 +829,7 @@ export const CreatePage = () => { } const arrayBuffer = await generateZipFile(zip) - if (!arrayBuffer) { - setIsLoading(false) - return - } + if (!arrayBuffer) return setLoadingSpinnerDesc('Encrypting zip file') const encryptedArrayBuffer = await encryptZipFile( @@ -917,7 +893,7 @@ export const CreatePage = () => {
    {selectedFiles.length > 0 && selectedFiles.map((file, index) => ( -
    { @@ -925,32 +901,32 @@ export const CreatePage = () => { setCurrentFile(file) }} > - <> - {file.name} - - -
    + {file.name} + + ))}
+
} right={ From 5c8cbc195603b21612f8bcb3979c32d8d3742080 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 28 Aug 2024 16:51:02 +0200 Subject: [PATCH 28/58] refactor(toolbox): responsiveness and cleanup --- src/pages/create/index.tsx | 50 ++++++++++++++---------------- src/pages/create/style.module.scss | 24 ++++++++++---- src/types/drawing.ts | 3 +- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 2c32f7d..bf6c26f 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -132,109 +132,109 @@ export const CreatePage = () => { const [toolbox] = useState([ { identifier: MarkType.TEXT, - icon: , + icon: faT, label: 'Text', active: true }, { identifier: MarkType.SIGNATURE, - icon: , + icon: faSignature, label: 'Signature', active: false }, { identifier: MarkType.JOBTITLE, - icon: , + icon: faBriefcase, label: 'Job Title', active: false }, { identifier: MarkType.FULLNAME, - icon: , + icon: faIdCard, label: 'Full Name', active: false }, { identifier: MarkType.INITIALS, - icon: , + icon: faHeading, label: 'Initials', active: false }, { identifier: MarkType.DATETIME, - icon: , + icon: faClock, label: 'Date Time', active: false }, { identifier: MarkType.DATE, - icon: , + icon: faCalendarDays, label: 'Date', active: false }, { identifier: MarkType.NUMBER, - icon: , + icon: fa1, label: 'Number', active: false }, { identifier: MarkType.IMAGES, - icon: , + icon: faImage, label: 'Images', active: false }, { identifier: MarkType.CHECKBOX, - icon: , + icon: faSquareCheck, label: 'Checkbox', active: false }, { identifier: MarkType.MULTIPLE, - icon: , + icon: faCheckDouble, label: 'Multiple', active: false }, { identifier: MarkType.FILE, - icon: , + icon: faPaperclip, label: 'File', active: false }, { identifier: MarkType.RADIO, - icon: , + icon: faCircleDot, label: 'Radio', active: false }, { identifier: MarkType.SELECT, - icon: , + icon: faSquareCaretDown, label: 'Select', active: false }, { identifier: MarkType.CELLS, - icon: , + icon: faTableCellsLarge, label: 'Cells', active: false }, { identifier: MarkType.STAMP, - icon: , + icon: faStamp, label: 'Stamp', active: false }, { identifier: MarkType.PAYMENT, - icon: , + icon: faCreditCard, label: 'Payment', active: false }, { identifier: MarkType.PHONE, - icon: , + icon: faPhone, label: 'Phone', active: false } @@ -1020,20 +1020,16 @@ export const CreatePage = () => { return (
{ - handleToolSelect(drawTool) - } - : () => null - } + {...(drawTool.active && { + onClick: () => handleToolSelect(drawTool) + })} className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${!drawTool.active ? styles.comingSoon : ''} `} > - {drawTool.icon} + {drawTool.label} {drawTool.active ? ( - + ) : ( Date: Wed, 28 Aug 2024 18:05:30 +0200 Subject: [PATCH 29/58] refactor(sticky-columns-layout): initial responsiveness and breakpoints --- src/layouts/StickySideColumns.module.scss | 34 +++++++++++++++++++---- src/pages/create/style.module.scss | 4 +-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/layouts/StickySideColumns.module.scss b/src/layouts/StickySideColumns.module.scss index 77daa03..4defd59 100644 --- a/src/layouts/StickySideColumns.module.scss +++ b/src/layouts/StickySideColumns.module.scss @@ -3,9 +3,26 @@ .container { display: grid; - grid-template-columns: 0.75fr 1.5fr 0.75fr; - grid-gap: 30px; - flex-grow: 1; + + @media only screen and (max-width: 767px) { + gap: 20px; + grid-auto-flow: column; + grid-auto-columns: 100%; + + overflow-x: auto; + overscroll-behavior-inline: contain; + + scroll-snap-type: inline mandatory; + + > * { + scroll-snap-align: start; + } + } + + @media only screen and (min-width: 768px) { + grid-template-columns: 0.75fr 1.5fr 0.75fr; + gap: 30px; + } } .sidesWrap { @@ -16,8 +33,10 @@ } .sides { - position: sticky; - top: $header-height + $body-vertical-padding; + @media only screen and (min-width: 768px) { + position: sticky; + top: $header-height + $body-vertical-padding; + } } .files { @@ -29,4 +48,9 @@ padding: 10px; border: 10px solid $overlay-background-color; border-radius: 4px; + + @media only screen and (max-width: 767px) { + max-height: calc(100svh - $header-height + $body-vertical-padding * 2); + overflow-y: auto; + } } diff --git a/src/pages/create/style.module.scss b/src/pages/create/style.module.scss index 0b7c8f7..0ea1c46 100644 --- a/src/pages/create/style.module.scss +++ b/src/pages/create/style.module.scss @@ -135,11 +135,11 @@ grid-template-columns: 1fr; @container (min-width: 204px) { - grid-template-columns: 1fr 1fr; + grid-template-columns: repeat(2, 1fr); } @container (min-width: 309px) { - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: repeat(3, 1fr); } gap: 15px; From a48751b9a8a7e1d37f300107ed668fb83ba57e74 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 29 Aug 2024 16:43:09 +0200 Subject: [PATCH 30/58] refactor(footer): make footer a portal and use as needed --- src/components/Footer/Footer.tsx | 205 ++++++++++--------- src/layouts/Main.tsx | 2 - src/pages/home/index.tsx | 2 + src/pages/landing/index.tsx | 2 + src/pages/profile/index.tsx | 2 + src/pages/settings/Settings.tsx | 102 ++++----- src/pages/settings/cache/index.tsx | 86 ++++---- src/pages/settings/profile/index.tsx | 2 + src/pages/settings/relays/index.tsx | 296 ++++++++++++++------------- src/pages/verify/index.tsx | 2 + 10 files changed, 362 insertions(+), 339 deletions(-) diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index eac4166..17140e4 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -4,125 +4,128 @@ import styles from './style.module.scss' import { Container } from '../Container' import nostrImage from '../../assets/images/nostr.gif' import { appPublicRoutes } from '../../routes' +import { createPortal } from 'react-dom' -export const Footer = () => ( -
- - + createPortal( +
+ - - Logo - - - - - + + + + - Source - + + - - - - - -
- Built by  - - Nostr Dev - {' '} - 2024. -
-
-) +
+
+ Built by  + + Nostr Dev + {' '} + 2024. +
+
, + document.getElementById('root')! + ) diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index ac233cc..f3962eb 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -26,7 +26,6 @@ import { } from '../utils' import { useAppSelector } from '../hooks' import styles from './style.module.scss' -import { Footer } from '../components/Footer/Footer' export const MainLayout = () => { const dispatch: Dispatch = useDispatch() @@ -160,7 +159,6 @@ export const MainLayout = () => { > -
) } diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index ddc777e..c93c9f8 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -18,6 +18,7 @@ import { SigitCardDisplayInfo, SigitStatus } from '../../utils' +import { Footer } from '../../components/Footer/Footer' // Unsupported Filter options are commented const FILTERS = [ @@ -262,6 +263,7 @@ export const HomePage = () => { ))}
+
) } diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx index 015d721..deae096 100644 --- a/src/pages/landing/index.tsx +++ b/src/pages/landing/index.tsx @@ -19,6 +19,7 @@ import { faWifi } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack' +import { Footer } from '../../components/Footer/Footer' export const LandingPage = () => { const navigate = useNavigate() @@ -162,6 +163,7 @@ export const LandingPage = () => { +
) } diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index a7b205b..8736c84 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -20,6 +20,7 @@ import { } from '../../utils' import styles from './style.module.scss' import { Container } from '../../components/Container' +import { Footer } from '../../components/Footer/Footer' export const ProfilePage = () => { const navigate = useNavigate() @@ -285,6 +286,7 @@ export const ProfilePage = () => {
)} +