From ed0158e8177b79a56124c57569f5cba81d57b40b Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 11 Aug 2024 22:19:26 +0300 Subject: [PATCH 01/63] feat(pdf-marking): updates design and functionality of the pdf marking form --- .../DrawPDFFields/style.module.scss | 4 + src/components/MarkFormField/index.tsx | 102 ++++++++++ .../MarkFormField/style.module.scss | 187 ++++++++++++++++++ src/components/PDFView/PdfMarkItem.tsx | 27 +-- src/components/PDFView/PdfMarking.tsx | 118 ++++++----- src/pages/sign/MarkFormField.tsx | 37 ---- src/types/mark.ts | 26 +-- src/utils/const.ts | 4 +- src/utils/mark.ts | 68 +++++-- 9 files changed, 450 insertions(+), 123 deletions(-) create mode 100644 src/components/MarkFormField/index.tsx create mode 100644 src/components/MarkFormField/style.module.scss delete mode 100644 src/pages/sign/MarkFormField.tsx diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index e3e7856..f1993eb 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -71,6 +71,10 @@ visibility: hidden; } + &.edited { + border: 1px dotted #01aaad + } + .resizeHandle { position: absolute; right: -5px; diff --git a/src/components/MarkFormField/index.tsx b/src/components/MarkFormField/index.tsx new file mode 100644 index 0000000..71f6668 --- /dev/null +++ b/src/components/MarkFormField/index.tsx @@ -0,0 +1,102 @@ +import { CurrentUserMark } from '../../types/mark.ts' +import styles from './style.module.scss' + +import { MARK_TYPE_TRANSLATION, NEXT, SIGN } from '../../utils/const.ts' +import { + findNextIncompleteCurrentUserMark, + isCurrentUserMarksComplete, + isCurrentValueLast +} from '../../utils' + +interface MarkFormFieldProps { + handleSubmit: (event: any) => void + handleSelectedMarkValueChange: (event: any) => void + selectedMark: CurrentUserMark + selectedMarkValue: string + currentUserMarks: CurrentUserMark[] + handleCurrentUserMarkChange: (mark: CurrentUserMark) => void +} + +/** + * Responsible for rendering a form field connected to a mark and keeping track of its value. + */ +const MarkFormField = ({ + handleSubmit, + handleSelectedMarkValueChange, + selectedMark, + selectedMarkValue, + currentUserMarks, + handleCurrentUserMarkChange +}: MarkFormFieldProps) => { + const getSubmitButtonText = () => (isReadyToSign() ? SIGN : NEXT) + const isReadyToSign = () => + isCurrentUserMarksComplete(currentUserMarks) || + isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue) + const isCurrent = (currentMark: CurrentUserMark) => + currentMark.id === selectedMark.id + const isDone = (currentMark: CurrentUserMark) => currentMark.isCompleted + const findNext = () => { + return ( + currentUserMarks[selectedMark.id] || + findNextIncompleteCurrentUserMark(currentUserMarks) + ) + } + const handleFormSubmit = (event: React.FormEvent) => { + event.preventDefault() + console.log('handle form submit runs...') + return isReadyToSign() + ? handleSubmit(event) + : handleCurrentUserMarkChange(findNext()!) + } + return ( +
+
+
+
+
+

Add your signature

+
+
+
+
handleFormSubmit(e)}> + +
+ +
+
+
+
+ {currentUserMarks.map((mark, index) => { + return ( +
+ + {isCurrent(mark) && ( +
+ )} +
+ ) + })} +
+
+
+
+
+
+ ) +} + +export default MarkFormField diff --git a/src/components/MarkFormField/style.module.scss b/src/components/MarkFormField/style.module.scss new file mode 100644 index 0000000..bff4644 --- /dev/null +++ b/src/components/MarkFormField/style.module.scss @@ -0,0 +1,187 @@ +.container { + width: 100%; + display: flex; + flex-direction: column; + position: fixed; + bottom: 0; + right: 0; + left: 0; + align-items: center; +} + +.actions { + background: white; + width: 100%; + border-radius: 4px; + padding: 10px 20px; + display: flex; + flex-direction: column; + align-items: center; + grid-gap: 15px; + box-shadow: 0 -2px 4px 0 rgb(0,0,0,0.1); + max-width: 750px; +} + +.actionsWrapper { + display: flex; + flex-direction: column; + grid-gap: 20px; + flex-grow: 1; + width: 100%; +} + +.actionsTop { + display: flex; + flex-direction: row; + grid-gap: 10px; + align-items: center; +} + +.actionsTopInfo { + flex-grow: 1; +} + +.actionsTopInfoText { + font-size: 16px; + color: #434343; +} + +.actionsTrigger { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + position: relative; +} + +.actionButtons { + display: flex; + flex-direction: row; + grid-gap: 5px; +} + +.inputWrapper { + display: flex; + flex-direction: column; + grid-gap: 10px; +} + +.textInput { + height: 100px; + background: rgba(0,0,0,0.1); + border-radius: 4px; + border: solid 2px #4c82a3; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.input { + border-radius: 4px; + border: solid 1px rgba(0,0,0,0.15); + padding: 5px 10px; + font-size: 16px; + width: 100%; + background: linear-gradient(rgba(0,0,0,0.00), rgba(0,0,0,0.00) 100%), linear-gradient(white, white); +} + +.input:focus { + border: solid 1px rgba(0,0,0,0.15); + outline: none; + background: linear-gradient(rgba(0,0,0,0.05), rgba(0,0,0,0.05) 100%), linear-gradient(white, white); +} + +.actionsBottom { + display: flex; + flex-direction: row; + grid-gap: 5px; + justify-content: center; + align-items: center; +} + +button { + transition: ease 0.2s; + width: auto; + border-radius: 4px; + outline: unset; + border: unset; + background: unset; + color: #ffffff; + background: #4c82a3; + font-weight: 500; + font-size: 14px; + padding: 8px 15px; + white-space: nowrap; + display: flex; + flex-direction: row; + grid-gap: 12px; + justify-content: center; + align-items: center; + text-decoration: unset; + position: relative; + cursor: pointer; +} + +button:hover { + transition: ease 0.2s; + background: #5e8eab; + color: white; +} + +button:active { + transition: ease 0.2s; + background: #447592; + color: white; +} + +.submitButton { + width: 100%; + max-width: 300px; +} + +.footerContainer { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.footer { + display: flex; + flex-direction: row; + grid-gap: 5px; + align-items: start; + justify-content: center; + width: 100%; +} + +.pagination { + display: flex; + flex-direction: column; + grid-gap: 5px; +} + +.paginationButton { + font-size: 12px; + padding: 5px 10px; + border-radius: 3px; + background: rgba(0,0,0,0.1); + color: rgba(0,0,0,0.5); +} + +.paginationButton:hover { + background: #447592; + color: rgba(255,255,255,0.5); +} + +.paginationButtonDone { + background: #5e8eab; + color: rgb(255,255,255); +} + +.paginationButtonCurrent { + height: 2px; + width: 100%; + background: #4c82a3; +} diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx index 7a2b24b..d93c2b2 100644 --- a/src/components/PDFView/PdfMarkItem.tsx +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -12,26 +12,31 @@ interface PdfMarkItemProps { /** * Responsible for display an individual Pdf Mark. */ -const PdfMarkItem = ({ selectedMark, handleMarkClick, selectedMarkValue, userMark }: PdfMarkItemProps) => { - const { location } = userMark.mark; - const handleClick = () => handleMarkClick(userMark.mark.id); - const getMarkValue = () => ( - selectedMark?.mark.id === userMark.mark.id - ? selectedMarkValue - : userMark.mark.value - ) +const PdfMarkItem = ({ + selectedMark, + handleMarkClick, + selectedMarkValue, + userMark +}: 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 return (
{getMarkValue()}
+ > + {getMarkValue()} + ) } -export default PdfMarkItem \ No newline at end of file +export default PdfMarkItem diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index a6dc1a4..e162062 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -1,22 +1,23 @@ import PdfView from './index.tsx' -import MarkFormField from '../../pages/sign/MarkFormField.tsx' +import MarkFormField from '../MarkFormField' import { PdfFile } from '../../types/drawing.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts' import React, { useState, useEffect } from 'react' import { - findNextCurrentUserMark, + findNextIncompleteCurrentUserMark, + getUpdatedMark, isCurrentUserMarksComplete, - updateCurrentUserMarks, + updateCurrentUserMarks } from '../../utils' import { EMPTY } from '../../utils/const.ts' import { Container } from '../Container' import styles from '../../pages/sign/style.module.scss' interface PdfMarkingProps { - files: { pdfFile: PdfFile, filename: string, hash: string | null }[], - currentUserMarks: CurrentUserMark[], - setIsReadyToSign: (isReadyToSign: boolean) => void, - setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void, + files: { pdfFile: PdfFile; filename: string; hash: string | null }[] + currentUserMarks: CurrentUserMark[] + setIsReadyToSign: (isReadyToSign: boolean) => void + setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void setUpdatedMarks: (markToUpdate: Mark) => void } @@ -35,66 +36,89 @@ const PdfMarking = (props: PdfMarkingProps) => { setUpdatedMarks } = props const [selectedMark, setSelectedMark] = useState(null) - const [selectedMarkValue, setSelectedMarkValue] = useState("") + const [selectedMarkValue, setSelectedMarkValue] = useState('') useEffect(() => { - setSelectedMark(findNextCurrentUserMark(currentUserMarks) || null) - }, [currentUserMarks]) + setSelectedMark(findNextIncompleteCurrentUserMark(currentUserMarks) || null) + }, []) const handleMarkClick = (id: number) => { - const nextMark = currentUserMarks.find((mark) => mark.mark.id === id); - setSelectedMark(nextMark!); - setSelectedMarkValue(nextMark?.mark.value ?? EMPTY); + const nextMark = currentUserMarks.find((mark) => mark.mark.id === id) + setSelectedMark(nextMark!) + setSelectedMarkValue(nextMark?.mark.value ?? EMPTY) + } + + const handleCurrentUserMarkChange = (mark: CurrentUserMark) => { + if (!selectedMark) return + const updatedSelectedMark: CurrentUserMark = getUpdatedMark( + selectedMark, + selectedMarkValue + ) + + const updatedCurrentUserMarks = updateCurrentUserMarks( + currentUserMarks, + updatedSelectedMark + ) + setCurrentUserMarks(updatedCurrentUserMarks) + setSelectedMark(mark) + setSelectedMarkValue(mark.currentValue ?? EMPTY) } const handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); - if (!selectedMarkValue || !selectedMark) return; + event.preventDefault() + if (!selectedMarkValue || !selectedMark) return - const updatedMark: CurrentUserMark = { - ...selectedMark, - mark: { - ...selectedMark.mark, - value: selectedMarkValue - }, - isCompleted: true - } + const updatedMark: CurrentUserMark = getUpdatedMark( + selectedMark, + selectedMarkValue + ) setSelectedMarkValue(EMPTY) - const updatedCurrentUserMarks = updateCurrentUserMarks(currentUserMarks, updatedMark); + const updatedCurrentUserMarks = updateCurrentUserMarks( + currentUserMarks, + updatedMark + ) setCurrentUserMarks(updatedCurrentUserMarks) - setSelectedMark(findNextCurrentUserMark(updatedCurrentUserMarks) || null) - console.log(isCurrentUserMarksComplete(updatedCurrentUserMarks)) - setIsReadyToSign(isCurrentUserMarksComplete(updatedCurrentUserMarks)) + setSelectedMark(null) + setIsReadyToSign(true) setUpdatedMarks(updatedMark.mark) } - const handleChange = (event: React.ChangeEvent) => setSelectedMarkValue(event.target.value) + // const updateCurrentUserMarkValues = () => { + // const updatedMark: CurrentUserMark = getUpdatedMark(selectedMark!, selectedMarkValue) + // const updatedCurrentUserMarks = updateCurrentUserMarks(currentUserMarks, updatedMark) + // setSelectedMarkValue(EMPTY) + // setCurrentUserMarks(updatedCurrentUserMarks) + // } + + const handleChange = (event: React.ChangeEvent) => + setSelectedMarkValue(event.target.value) return ( <> - { - currentUserMarks?.length > 0 && ( - )} - { - selectedMark !== null && ( - - )} + {currentUserMarks?.length > 0 && ( + + )} + {selectedMark !== null && ( + + )} ) } -export default PdfMarking \ No newline at end of file +export default PdfMarking diff --git a/src/pages/sign/MarkFormField.tsx b/src/pages/sign/MarkFormField.tsx deleted file mode 100644 index 4de82a4..0000000 --- a/src/pages/sign/MarkFormField.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { CurrentUserMark } from '../../types/mark.ts' -import styles from './style.module.scss' -import { Box, Button, TextField } from '@mui/material' - -import { MARK_TYPE_TRANSLATION } from '../../utils/const.ts' - -interface MarkFormFieldProps { - handleSubmit: (event: any) => void - handleChange: (event: any) => void - selectedMark: CurrentUserMark - selectedMarkValue: string -} - -/** - * Responsible for rendering a form field connected to a mark and keeping track of its value. - */ -const MarkFormField = (props: MarkFormFieldProps) => { - const { handleSubmit, handleChange, selectedMark, selectedMarkValue } = props; - const getSubmitButton = () => selectedMark.isLast ? 'Complete' : 'Next'; - return ( -
- - - - -
- ) -} - -export default MarkFormField; \ No newline at end of file diff --git a/src/types/mark.ts b/src/types/mark.ts index 3184f95..c0f9b88 100644 --- a/src/types/mark.ts +++ b/src/types/mark.ts @@ -1,24 +1,26 @@ -import { MarkType } from "./drawing"; +import { MarkType } from './drawing' export interface CurrentUserMark { + id: number mark: Mark isLast: boolean isCompleted: boolean + currentValue?: string } export interface Mark { - id: number; - npub: string; - pdfFileHash: string; - type: MarkType; - location: MarkLocation; - value?: string; + id: number + npub: string + pdfFileHash: string + type: MarkType + location: MarkLocation + value?: string } export interface MarkLocation { - top: number; - left: number; - height: number; - width: number; - page: number; + top: number + left: number + height: number + width: number + page: number } diff --git a/src/utils/const.ts b/src/utils/const.ts index 4f8c233..2a97dfe 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -3,4 +3,6 @@ import { MarkType } from '../types/drawing.ts' export const EMPTY: string = '' export const MARK_TYPE_TRANSLATION: { [key: string]: string } = { [MarkType.FULLNAME.valueOf()]: 'Full Name' -} \ No newline at end of file +} +export const SIGN: string = 'Sign' +export const NEXT: string = 'Next' diff --git a/src/utils/mark.ts b/src/utils/mark.ts index 13cff84..18cc3e8 100644 --- a/src/utils/mark.ts +++ b/src/utils/mark.ts @@ -2,6 +2,7 @@ import { CurrentUserMark, Mark } from '../types/mark.ts' import { hexToNpub } from './nostr.ts' import { Meta, SignedEventContent } from '../types' import { Event } from 'nostr-tools' +import { EMPTY } from './const.ts' /** * Takes in an array of Marks already filtered by User. @@ -9,16 +10,18 @@ import { Event } from 'nostr-tools' * @param marks - default Marks extracted from Meta * @param signedMetaMarks - signed user Marks extracted from DocSignatures */ -const getCurrentUserMarks = (marks: Mark[], signedMetaMarks: Mark[]): CurrentUserMark[] => { +const getCurrentUserMarks = ( + marks: Mark[], + signedMetaMarks: Mark[] +): CurrentUserMark[] => { return marks.map((mark, index, arr) => { - const signedMark = signedMetaMarks.find((m) => m.id === mark.id); - if (signedMark && !!signedMark.value) { - mark.value = signedMark.value - } + const signedMark = signedMetaMarks.find((m) => m.id === mark.id) return { mark, + currentValue: signedMark?.value ?? EMPTY, + id: index + 1, isLast: isLast(index, arr), - isCompleted: !!mark.value + isCompleted: !!signedMark?.value } }) } @@ -27,8 +30,10 @@ const getCurrentUserMarks = (marks: Mark[], signedMetaMarks: Mark[]): CurrentUse * Returns next incomplete CurrentUserMark if there is one * @param usersMarks */ -const findNextCurrentUserMark = (usersMarks: CurrentUserMark[]): CurrentUserMark | undefined => { - return usersMarks.find((mark) => !mark.isCompleted); +const findNextIncompleteCurrentUserMark = ( + usersMarks: CurrentUserMark[] +): CurrentUserMark | undefined => { + return usersMarks.find((mark) => !mark.isCompleted) } /** @@ -37,7 +42,7 @@ const findNextCurrentUserMark = (usersMarks: CurrentUserMark[]): CurrentUserMark * @param pubkey */ const filterMarksByPubkey = (marks: Mark[], pubkey: string): Mark[] => { - return marks.filter(mark => mark.npub === hexToNpub(pubkey)) + return marks.filter((mark) => mark.npub === hexToNpub(pubkey)) } /** @@ -57,7 +62,9 @@ const extractMarksFromSignedMeta = (meta: Meta): Mark[] => { * marked as complete. * @param currentUserMarks */ -const isCurrentUserMarksComplete = (currentUserMarks: CurrentUserMark[]): boolean => { +const isCurrentUserMarksComplete = ( + currentUserMarks: CurrentUserMark[] +): boolean => { return currentUserMarks.every((mark) => mark.isCompleted) } @@ -68,7 +75,7 @@ const isCurrentUserMarksComplete = (currentUserMarks: CurrentUserMark[]): boolea * @param markToUpdate */ const updateMarks = (marks: Mark[], markToUpdate: Mark): Mark[] => { - const indexToUpdate = marks.findIndex(mark => mark.id === markToUpdate.id); + const indexToUpdate = marks.findIndex((mark) => mark.id === markToUpdate.id) return [ ...marks.slice(0, indexToUpdate), markToUpdate, @@ -76,8 +83,13 @@ const updateMarks = (marks: Mark[], markToUpdate: Mark): Mark[] => { ] } -const updateCurrentUserMarks = (currentUserMarks: CurrentUserMark[], markToUpdate: CurrentUserMark): CurrentUserMark[] => { - const indexToUpdate = currentUserMarks.findIndex((m) => m.mark.id === markToUpdate.mark.id) +const updateCurrentUserMarks = ( + currentUserMarks: CurrentUserMark[], + markToUpdate: CurrentUserMark +): CurrentUserMark[] => { + const indexToUpdate = currentUserMarks.findIndex( + (m) => m.mark.id === markToUpdate.mark.id + ) return [ ...currentUserMarks.slice(0, indexToUpdate), markToUpdate, @@ -85,14 +97,40 @@ const updateCurrentUserMarks = (currentUserMarks: CurrentUserMark[], markToUpdat ] } -const isLast = (index: number, arr: T[]) => (index === (arr.length -1)) +const isLast = (index: number, arr: T[]) => index === arr.length - 1 + +const isCurrentValueLast = ( + currentUserMarks: CurrentUserMark[], + selectedMark: CurrentUserMark, + selectedMarkValue: string +) => { + const filteredMarks = currentUserMarks.filter( + (mark) => mark.id !== selectedMark.id + ) + return ( + isCurrentUserMarksComplete(filteredMarks) && selectedMarkValue.length > 0 + ) +} + +const getUpdatedMark = ( + selectedMark: CurrentUserMark, + selectedMarkValue: string +): CurrentUserMark => { + return { + ...selectedMark, + currentValue: selectedMarkValue, + isCompleted: !!selectedMarkValue + } +} export { getCurrentUserMarks, filterMarksByPubkey, extractMarksFromSignedMeta, isCurrentUserMarksComplete, - findNextCurrentUserMark, + findNextIncompleteCurrentUserMark, updateMarks, updateCurrentUserMarks, + isCurrentValueLast, + getUpdatedMark } From 0d52cd71134c9b3eee41e1ea853dba65afc7c79b Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 12 Aug 2024 10:16:30 +0300 Subject: [PATCH 02/63] fix: selected mark selection --- src/components/MarkFormField/style.module.scss | 1 + src/components/PDFView/PdfMarking.tsx | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/MarkFormField/style.module.scss b/src/components/MarkFormField/style.module.scss index bff4644..e4e76ea 100644 --- a/src/components/MarkFormField/style.module.scss +++ b/src/components/MarkFormField/style.module.scss @@ -138,6 +138,7 @@ button:active { .submitButton { width: 100%; max-width: 300px; + margin-top: 10px; } .footerContainer { diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index e162062..b0e0748 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -39,8 +39,13 @@ const PdfMarking = (props: PdfMarkingProps) => { const [selectedMarkValue, setSelectedMarkValue] = useState('') useEffect(() => { - setSelectedMark(findNextIncompleteCurrentUserMark(currentUserMarks) || null) - }, []) + if (selectedMark === null && currentUserMarks.length > 0) { + setSelectedMark( + findNextIncompleteCurrentUserMark(currentUserMarks) || + currentUserMarks[0] + ) + } + }, [currentUserMarks, selectedMark]) const handleMarkClick = (id: number) => { const nextMark = currentUserMarks.find((mark) => mark.mark.id === id) @@ -60,8 +65,8 @@ const PdfMarking = (props: PdfMarkingProps) => { updatedSelectedMark ) setCurrentUserMarks(updatedCurrentUserMarks) - setSelectedMark(mark) setSelectedMarkValue(mark.currentValue ?? EMPTY) + setSelectedMark(mark) } const handleSubmit = (event: React.FormEvent) => { From 4c04c1240344c0f07a503bb0a5f56e7bfc791c8f Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 12 Aug 2024 12:08:53 +0300 Subject: [PATCH 03/63] fix: button colour --- src/components/MarkFormField/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/MarkFormField/index.tsx b/src/components/MarkFormField/index.tsx index 71f6668..15ecc2a 100644 --- a/src/components/MarkFormField/index.tsx +++ b/src/components/MarkFormField/index.tsx @@ -34,7 +34,8 @@ const MarkFormField = ({ isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue) const isCurrent = (currentMark: CurrentUserMark) => currentMark.id === selectedMark.id - const isDone = (currentMark: CurrentUserMark) => currentMark.isCompleted + const isDone = (currentMark: CurrentUserMark) => + isCurrent(currentMark) ? !!selectedMarkValue : currentMark.isCompleted const findNext = () => { return ( currentUserMarks[selectedMark.id] || @@ -50,6 +51,9 @@ const MarkFormField = ({ } return (
+
+ +
From 3549b6e54292b3d6fe456025cefc397ba0aa070d Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 12 Aug 2024 12:32:36 +0300 Subject: [PATCH 04/63] fix: toggle --- src/components/MarkFormField/index.tsx | 22 ++++++++++++++++-- .../MarkFormField/style.module.scss | 23 ++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/components/MarkFormField/index.tsx b/src/components/MarkFormField/index.tsx index 15ecc2a..2fa2780 100644 --- a/src/components/MarkFormField/index.tsx +++ b/src/components/MarkFormField/index.tsx @@ -7,6 +7,7 @@ import { isCurrentUserMarksComplete, isCurrentValueLast } from '../../utils' +import { useState } from 'react' interface MarkFormFieldProps { handleSubmit: (event: any) => void @@ -28,6 +29,7 @@ const MarkFormField = ({ currentUserMarks, handleCurrentUserMarkChange }: MarkFormFieldProps) => { + const [displayActions, setDisplayActions] = useState(true) const getSubmitButtonText = () => (isReadyToSign() ? SIGN : NEXT) const isReadyToSign = () => isCurrentUserMarksComplete(currentUserMarks) || @@ -49,12 +51,28 @@ const MarkFormField = ({ ? handleSubmit(event) : handleCurrentUserMarkChange(findNext()!) } + const toggleActions = () => setDisplayActions(!displayActions) return (
- +
-
+
diff --git a/src/components/MarkFormField/style.module.scss b/src/components/MarkFormField/style.module.scss index e4e76ea..f3fa5d5 100644 --- a/src/components/MarkFormField/style.module.scss +++ b/src/components/MarkFormField/style.module.scss @@ -14,12 +14,16 @@ width: 100%; border-radius: 4px; padding: 10px 20px; - display: flex; + display: none; flex-direction: column; align-items: center; grid-gap: 15px; box-shadow: 0 -2px 4px 0 rgb(0,0,0,0.1); max-width: 750px; + + &.expanded { + display: flex; + } } .actionsWrapper { @@ -186,3 +190,20 @@ button:active { width: 100%; background: #4c82a3; } + +.trigger { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + position: relative; +} + +.triggerBtn { + background: white; + color: #434343; + padding: 5px 30px; + box-shadow: 0px -3px 4px 0 rgb(0,0,0,0.1); + position: absolute; + top: -25px; +} From 6d881ccb45e440c343b768c6d26d6933b2a4b813 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 13 Aug 2024 12:48:52 +0300 Subject: [PATCH 05/63] feat(pdf-marking): adds file downloading functionality --- src/components/FileList/index.tsx | 41 +++++++ src/components/FileList/style.module.scss | 78 ++++++++++++ src/components/PDFView/PdfMarking.tsx | 54 +++++--- src/components/PDFView/index.tsx | 49 +++++--- src/components/PDFView/style.module.scss | 15 ++- src/pages/sign/index.tsx | 98 ++++++++++----- src/pages/verify/index.tsx | 31 ++--- src/types/file.ts | 8 ++ src/utils/const.ts | 2 + src/utils/file.ts | 24 ++++ src/utils/pdf.ts | 143 +++++++++++----------- src/utils/utils.ts | 16 ++- 12 files changed, 402 insertions(+), 157 deletions(-) create mode 100644 src/components/FileList/index.tsx create mode 100644 src/components/FileList/style.module.scss create mode 100644 src/types/file.ts create mode 100644 src/utils/file.ts diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx new file mode 100644 index 0000000..ba69332 --- /dev/null +++ b/src/components/FileList/index.tsx @@ -0,0 +1,41 @@ +import { CurrentUserFile } from '../../types/file.ts' +import styles from './style.module.scss' +import { Button } from '@mui/material' + +interface FileListProps { + files: CurrentUserFile[] + currentFile: CurrentUserFile + setCurrentFile: (file: CurrentUserFile) => void + handleDownload: () => void +} + +const FileList = ({ + files, + currentFile, + setCurrentFile, + handleDownload +}: FileListProps) => { + const isActive = (file: CurrentUserFile) => file.id === currentFile.id + return ( +
+
+
    + {files.map((file: CurrentUserFile) => ( +
  • setCurrentFile(file)} + > + {file.id} + {file.filename} +
  • + ))} +
+ +
+ ) +} + +export default FileList diff --git a/src/components/FileList/style.module.scss b/src/components/FileList/style.module.scss new file mode 100644 index 0000000..bbf9311 --- /dev/null +++ b/src/components/FileList/style.module.scss @@ -0,0 +1,78 @@ +.container { + border-radius: 4px; + background: white; + padding: 15px; + display: flex; + flex-direction: column; + grid-gap: 0px; +} + +.files { + display: flex; + flex-direction: column; + width: 100%; + grid-gap: 15px; + max-height: 350px; + overflow: auto; + padding: 0 5px 0 0; + margin: 0 -5px 0 0; +} + +.files::-webkit-scrollbar { + width: 10px; +} + +.files::-webkit-scrollbar-track { + background-color: rgba(0,0,0,0.15); +} + +.files::-webkit-scrollbar-thumb { + max-width: 10px; + border-radius: 2px; + background-color: #4c82a3; + cursor: grab; +} + +.fileItem { + transition: ease 0.2s; + display: flex; + flex-direction: row; + grid-gap: 10px; + border-radius: 4px; + overflow: hidden; + background: #ffffff; + padding: 5px 10px; + align-items: center; + color: rgba(0,0,0,0.5); + cursor: pointer; + flex-grow: 1; + font-size: 16px; + font-weight: 500; +} + +.fileItem:hover { + transition: ease 0.2s; + background: #4c82a3; + color: white; + + &.active { + background: #4c82a3; + color: white; + } +} + +.fileName { + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + -webkit-line-clamp: 1; +} + +.fileNumber { + font-size: 14px; + font-weight: 500; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index b0e0748..fc49f79 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -1,24 +1,26 @@ import PdfView from './index.tsx' import MarkFormField from '../MarkFormField' -import { PdfFile } from '../../types/drawing.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts' import React, { useState, useEffect } from 'react' import { findNextIncompleteCurrentUserMark, getUpdatedMark, - isCurrentUserMarksComplete, updateCurrentUserMarks } from '../../utils' import { EMPTY } from '../../utils/const.ts' import { Container } from '../Container' -import styles from '../../pages/sign/style.module.scss' +import signPageStyles from '../../pages/sign/style.module.scss' +import styles from './style.module.scss' +import { CurrentUserFile } from '../../types/file.ts' +import FileList from '../FileList' interface PdfMarkingProps { - files: { pdfFile: PdfFile; filename: string; hash: string | null }[] + files: CurrentUserFile[] currentUserMarks: CurrentUserMark[] setIsReadyToSign: (isReadyToSign: boolean) => void setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void setUpdatedMarks: (markToUpdate: Mark) => void + handleDownload: () => void } /** @@ -33,10 +35,12 @@ const PdfMarking = (props: PdfMarkingProps) => { currentUserMarks, setIsReadyToSign, setCurrentUserMarks, - setUpdatedMarks + setUpdatedMarks, + handleDownload } = props const [selectedMark, setSelectedMark] = useState(null) const [selectedMarkValue, setSelectedMarkValue] = useState('') + const [currentFile, setCurrentFile] = useState(null) useEffect(() => { if (selectedMark === null && currentUserMarks.length > 0) { @@ -47,6 +51,12 @@ const PdfMarking = (props: PdfMarkingProps) => { } }, [currentUserMarks, selectedMark]) + useEffect(() => { + if (currentFile === null && files.length > 0) { + setCurrentFile(files[0]) + } + }, [files, currentFile]) + const handleMarkClick = (id: number) => { const nextMark = currentUserMarks.find((mark) => mark.mark.id === id) setSelectedMark(nextMark!) @@ -101,16 +111,30 @@ const PdfMarking = (props: PdfMarkingProps) => { return ( <> - - {currentUserMarks?.length > 0 && ( - - )} + +
+
+ {currentFile !== null && ( + + )} +
+ {currentUserMarks?.length > 0 && ( +
+ +
+ )} +
{selectedMark !== null && ( void selectedMarkValue: string @@ -14,29 +14,38 @@ interface PdfViewProps { /** * Responsible for rendering Pdf files. */ -const PdfView = ({ files, currentUserMarks, handleMarkClick, selectedMarkValue, selectedMark }: PdfViewProps) => { - const filterByFile = (currentUserMarks: CurrentUserMark[], hash: string): CurrentUserMark[] => { - return currentUserMarks.filter((currentUserMark) => currentUserMark.mark.pdfFileHash === hash) +const PdfView = ({ + files, + currentUserMarks, + handleMarkClick, + selectedMarkValue, + selectedMark +}: PdfViewProps) => { + const filterByFile = ( + currentUserMarks: CurrentUserMark[], + hash: string + ): CurrentUserMark[] => { + return currentUserMarks.filter( + (currentUserMark) => currentUserMark.mark.pdfFileHash === hash + ) } return ( - { - files.map(({ pdfFile, hash }, i) => { - if (!hash) return - return ( - { + if (!hash) return + return ( + - ) - }) - } + ) + })} ) } -export default PdfView; \ No newline at end of file +export default PdfView diff --git a/src/components/PDFView/style.module.scss b/src/components/PDFView/style.module.scss index 2e6e519..3ebbc85 100644 --- a/src/components/PDFView/style.module.scss +++ b/src/components/PDFView/style.module.scss @@ -13,4 +13,17 @@ max-width: 100%; max-height: 100%; object-fit: contain; /* Ensure the image fits within the container */ -} \ No newline at end of file +} + +.container { + display: flex; + width: 100%; + flex-direction: column; +} + +.pdfView { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +} diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 07c385e..02ac415 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -16,13 +16,16 @@ import { State } from '../../store/rootReducer' import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types' import { decryptArrayBuffer, - encryptArrayBuffer, extractMarksFromSignedMeta, + encryptArrayBuffer, + extractMarksFromSignedMeta, extractZipUrlAndEncryptionKey, generateEncryptionKey, - generateKeysFile, getFilesWithHashes, + generateKeysFile, + getCurrentUserFiles, getHash, hexToNpub, - isOnline, loadZip, + isOnline, + loadZip, now, npubToHex, parseJson, @@ -41,9 +44,12 @@ import { getLastSignersSig } from '../../utils/sign.ts' import { filterMarksByPubkey, getCurrentUserMarks, - isCurrentUserMarksComplete, updateMarks + isCurrentUserMarksComplete, + updateMarks } from '../../utils' import PdfMarking from '../../components/PDFView/PdfMarking.tsx' +import { getZipWithFiles } from '../../utils/file.ts' +import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts' enum SignedStatus { Fully_Signed, User_Is_Next_Signer, @@ -81,7 +87,7 @@ export const SignPage = () => { const [signers, setSigners] = useState<`npub1${string}`[]>([]) const [viewers, setViewers] = useState<`npub1${string}`[]>([]) - const [marks, setMarks] = useState([]) + const [marks, setMarks] = useState([]) const [creatorFileHashes, setCreatorFileHashes] = useState<{ [key: string]: string }>({}) @@ -100,8 +106,10 @@ export const SignPage = () => { const [authUrl, setAuthUrl] = useState() const nostrController = NostrController.getInstance() - const [currentUserMarks, setCurrentUserMarks] = useState([]); - const [isReadyToSign, setIsReadyToSign] = useState(false); + const [currentUserMarks, setCurrentUserMarks] = useState( + [] + ) + const [isReadyToSign, setIsReadyToSign] = useState(false) useEffect(() => { if (signers.length > 0) { @@ -192,13 +200,16 @@ export const SignPage = () => { setViewers(createSignatureContent.viewers) setCreatorFileHashes(createSignatureContent.fileHashes) setSubmittedBy(createSignatureEvent.pubkey) - setMarks(createSignatureContent.markConfig); + setMarks(createSignatureContent.markConfig) if (usersPubkey) { - const metaMarks = filterMarksByPubkey(createSignatureContent.markConfig, usersPubkey!) + const metaMarks = filterMarksByPubkey( + createSignatureContent.markConfig, + usersPubkey! + ) const signedMarks = extractMarksFromSignedMeta(meta) - const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks); - setCurrentUserMarks(currentUserMarks); + const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks) + setCurrentUserMarks(currentUserMarks) // setCurrentUserMark(findNextCurrentUserMark(currentUserMarks) || null) setIsReadyToSign(isCurrentUserMarksComplete(currentUserMarks)) } @@ -307,7 +318,7 @@ export const SignPage = () => { ) if (arrayBuffer) { - files[fileName] = await convertToPdfFile(arrayBuffer, fileName); + files[fileName] = await convertToPdfFile(arrayBuffer, fileName) const hash = await getHash(arrayBuffer) if (hash) { @@ -348,7 +359,7 @@ export const SignPage = () => { const decrypt = async (file: File) => { setLoadingSpinnerDesc('Decrypting file') - const zip = await loadZip(file); + const zip = await loadZip(file) if (!zip) return const parsedKeysJson = await parseKeysJson(zip) @@ -414,6 +425,27 @@ export const SignPage = () => { return null } + const handleDownload = async () => { + if (Object.entries(files).length === 0 || !meta || !usersPubkey) return + setLoadingSpinnerDesc('Generating file') + try { + const zip = await getZipWithFiles(meta, files) + const arrayBuffer = await zip.generateAsync({ + type: ARRAY_BUFFER, + compression: DEFLATE, + compressionOptions: { + level: 6 + } + }) + if (!arrayBuffer) return + const blob = new Blob([arrayBuffer]) + saveAs(blob, `exported-${now()}.sigit.zip`) + } catch (error: any) { + console.log('error in zip:>> ', error) + toast.error(error.message || 'Error occurred in generating zip file') + } + } + const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => { const decryptedZipFile = new File([arrayBuffer], 'decrypted.zip') @@ -439,7 +471,7 @@ export const SignPage = () => { ) if (arrayBuffer) { - files[fileName] = await convertToPdfFile(arrayBuffer, fileName); + files[fileName] = await convertToPdfFile(arrayBuffer, fileName) const hash = await getHash(arrayBuffer) if (hash) { @@ -520,7 +552,10 @@ export const SignPage = () => { } // Sign the event for the meta file - const signEventForMeta = async (signerContent: { prevSig: string, marks: Mark[] }) => { + const signEventForMeta = async (signerContent: { + prevSig: string + marks: Mark[] + }) => { return await signEventForMetaFile( JSON.stringify(signerContent), nostrController, @@ -529,8 +564,8 @@ export const SignPage = () => { } const getSignerMarksForMeta = (): Mark[] | undefined => { - if (currentUserMarks.length === 0) return; - return currentUserMarks.map(( { mark }: CurrentUserMark) => mark); + if (currentUserMarks.length === 0) return + return currentUserMarks.map(({ mark }: CurrentUserMark) => mark) } // Update the meta signatures @@ -600,13 +635,9 @@ export const SignPage = () => { if (!arraybuffer) return null - return new File( - [new Blob([arraybuffer])], - `${unixNow}.sigit.zip`, - { - type: 'application/zip' - } - ) + return new File([new Blob([arraybuffer])], `${unixNow}.sigit.zip`, { + type: 'application/zip' + }) } // Handle errors during zip file generation @@ -694,7 +725,7 @@ export const SignPage = () => { setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') - if (!meta) return; + if (!meta) return const prevSig = getLastSignersSig(meta, signers) if (!prevSig) return @@ -918,11 +949,14 @@ export const SignPage = () => { ) } - return + return ( + + ) } diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 6290c02..b39fe74 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -23,14 +23,17 @@ import { SignedEventContent } from '../../types' import { - decryptArrayBuffer, extractMarksFromSignedMeta, + decryptArrayBuffer, + extractMarksFromSignedMeta, extractZipUrlAndEncryptionKey, getHash, - hexToNpub, now, + hexToNpub, + now, npubToHex, parseJson, readContentOfZipEntry, - shorten, signEventForMetaFile + shorten, + signEventForMetaFile } from '../../utils' import styles from './style.module.scss' import { Cancel, CheckCircle } from '@mui/icons-material' @@ -41,7 +44,7 @@ import { addMarks, convertToPdfBlob, convertToPdfFile, - groupMarksByPage, + groupMarksByPage } from '../../utils/pdf.ts' import { State } from '../../store/rootReducer.ts' import { useSelector } from 'react-redux' @@ -78,7 +81,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 }>({}) const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( {} @@ -155,7 +158,10 @@ export const VerifyPage = () => { ) if (arrayBuffer) { - files[fileName] = await convertToPdfFile(arrayBuffer, fileName!) + files[fileName] = await convertToPdfFile( + arrayBuffer, + fileName! + ) const hash = await getHash(arrayBuffer) if (hash) { @@ -169,7 +175,6 @@ export const VerifyPage = () => { setCurrentFileHashes(fileHashes) setFiles(files) - setSigners(createSignatureContent.signers) setViewers(createSignatureContent.viewers) setCreatorFileHashes(createSignatureContent.fileHashes) @@ -177,8 +182,6 @@ export const VerifyPage = () => { setMeta(metaInNavState) setIsLoading(false) - - } }) .catch((err) => { @@ -381,7 +384,7 @@ export const VerifyPage = () => { } const handleExport = async () => { - if (Object.entries(files).length === 0 ||!meta ||!usersPubkey) return; + if (Object.entries(files).length === 0 || !meta || !usersPubkey) return const usersNpub = hexToNpub(usersPubkey) if ( @@ -395,10 +398,8 @@ export const VerifyPage = () => { setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') - if (!meta) return; - const prevSig = getLastSignersSig(meta, signers) - if (!prevSig) return; + if (!prevSig) return const signedEvent = await signEventForMetaFile( JSON.stringify({ prevSig }), @@ -406,10 +407,10 @@ export const VerifyPage = () => { setIsLoading ) - if (!signedEvent) return; + if (!signedEvent) return const exportSignature = JSON.stringify(signedEvent, null, 2) - const updatedMeta = {...meta, exportSignature } + const updatedMeta = { ...meta, exportSignature } const stringifiedMeta = JSON.stringify(updatedMeta, null, 2) const zip = new JSZip() diff --git a/src/types/file.ts b/src/types/file.ts new file mode 100644 index 0000000..0f2c127 --- /dev/null +++ b/src/types/file.ts @@ -0,0 +1,8 @@ +import { PdfFile } from './drawing.ts' + +export interface CurrentUserFile { + id: number + pdfFile: PdfFile + filename: string + hash?: string +} diff --git a/src/utils/const.ts b/src/utils/const.ts index 2a97dfe..511f418 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -6,3 +6,5 @@ export const MARK_TYPE_TRANSLATION: { [key: string]: string } = { } export const SIGN: string = 'Sign' export const NEXT: string = 'Next' +export const ARRAY_BUFFER = 'arraybuffer' +export const DEFLATE = 'DEFLATE' diff --git a/src/utils/file.ts b/src/utils/file.ts new file mode 100644 index 0000000..94308d5 --- /dev/null +++ b/src/utils/file.ts @@ -0,0 +1,24 @@ +import { Meta } from '../types' +import { extractMarksFromSignedMeta } from './mark.ts' +import { addMarks, convertToPdfBlob, groupMarksByPage } from './pdf.ts' +import JSZip from 'jszip' +import { PdfFile } from '../types/drawing.ts' + +const getZipWithFiles = async ( + meta: Meta, + files: { [filename: string]: PdfFile } +): Promise => { + const zip = new JSZip() + const marks = extractMarksFromSignedMeta(meta) + const marksByPage = groupMarksByPage(marks) + + for (const [fileName, pdf] of Object.entries(files)) { + const pages = await addMarks(pdf.file, marksByPage) + const blob = await convertToPdfBlob(pages) + zip.file(`/files/${fileName}`, blob) + } + + return zip +} + +export { getZipWithFiles } diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index 70b6539..a684d78 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -3,13 +3,14 @@ import * as PDFJS from 'pdfjs-dist' import { PDFDocument } from 'pdf-lib' import { Mark } from '../types/mark.ts' -PDFJS.GlobalWorkerOptions.workerSrc = 'node_modules/pdfjs-dist/build/pdf.worker.mjs'; +PDFJS.GlobalWorkerOptions.workerSrc = + 'node_modules/pdfjs-dist/build/pdf.worker.mjs' /** * Scale between the PDF page's natural size and rendered size * @constant {number} */ -const SCALE: number = 3; +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 @@ -17,20 +18,20 @@ 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; +const FONT_SIZE: number = 40 /** * Current font type used when generating a PDF. */ -const FONT_TYPE: string = 'Arial'; +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 */ -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): File => { + const blob = new Blob([arrayBuffer], { type: 'application/pdf' }) + return new File([blob], fileName, { type: 'application/pdf' }) } /** @@ -50,42 +51,40 @@ const toPdfFile = async (file: File): Promise => { * @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)); + return Promise.all(selectedFiles.filter(isPdf).map(toPdfFile)) } /** * A utility that transforms a drawing coordinate number into a CSS-compatible string * @param coordinate */ -const inPx = (coordinate: number): string => `${coordinate}px`; +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'); +const isPdf = (file: File) => file.type.toLowerCase().includes('pdf') /** * Reads the pdf file binaries */ const readPdf = (file: File): Promise => { return new Promise((resolve, reject) => { - const reader = new FileReader(); + const reader = new FileReader() reader.onload = (e: any) => { const data = e.target.result resolve(data) - }; + } reader.onerror = (err) => { console.error('err', err) reject(err) - }; + } - reader.readAsDataURL(file); + reader.readAsDataURL(file) }) } @@ -94,26 +93,28 @@ const readPdf = (file: File): Promise => { * @param data pdf file bytes */ const pdfToImages = async (data: any): Promise => { - const images: string[] = []; - const pdf = await PDFJS.getDocument(data).promise; - const canvas = document.createElement("canvas"); + const images: string[] = [] + 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 context = canvas.getContext("2d"); - canvas.height = viewport.height; - canvas.width = viewport.width; - await page.render({ canvasContext: context!, viewport: viewport }).promise; - images.push(canvas.toDataURL()); + const page = await pdf.getPage(i + 1) + const viewport = page.getViewport({ scale: 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()) } - return Promise.resolve(images.map((image) => { - return { - image, - drawnFields: [] - } - })) + return Promise.resolve( + images.map((image) => { + return { + image, + drawnFields: [] + } + }) + ) } /** @@ -121,34 +122,37 @@ const pdfToImages = async (data: any): 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 (file: File, marksPerPage: {[key: string]: Mark[]}) => { - const p = await readPdf(file); - const pdf = await PDFJS.getDocument(p).promise; - const canvas = document.createElement("canvas"); +const addMarks = async ( + file: File, + marksPerPage: { [key: string]: Mark[] } +) => { + const p = await readPdf(file) + const pdf = await PDFJS.getDocument(p).promise + const canvas = document.createElement('canvas') - const images: string[] = []; + const images: string[] = [] - for (let i = 0; i< pdf.numPages; i++) { - const page = await pdf.getPage(i+1) - const viewport = page.getViewport({ scale: SCALE }); - const context = canvas.getContext("2d"); - canvas.height = viewport.height; - canvas.width = viewport.width; - await page.render({ canvasContext: context!, viewport: viewport }).promise; + for (let i = 0; i < pdf.numPages; i++) { + const page = await pdf.getPage(i + 1) + const viewport = page.getViewport({ scale: SCALE }) + const context = canvas.getContext('2d') + canvas.height = viewport.height + canvas.width = viewport.width + await page.render({ canvasContext: context!, viewport: viewport }).promise - marksPerPage[i].forEach(mark => draw(mark, context!)) + marksPerPage[i]?.forEach((mark) => draw(mark, context!)) - images.push(canvas.toDataURL()); + images.push(canvas.toDataURL()) } - return Promise.resolve(images); + return Promise.resolve(images) } /** * Utility to scale mark in line with the PDF-to-PNG scale */ const scaleMark = (mark: Mark): Mark => { - const { location } = mark; + const { location } = mark return { ...mark, location: { @@ -165,7 +169,7 @@ const scaleMark = (mark: Mark): Mark => { * Utility to check if a Mark has value * @param mark */ -const hasValue = (mark: Mark): boolean => !!mark.value; +const hasValue = (mark: Mark): boolean => !!mark.value /** * Draws a Mark on a Canvas representation of a PDF Page @@ -173,14 +177,14 @@ const hasValue = (mark: Mark): boolean => !!mark.value; * @param ctx a Canvas representation of a specific PDF Page */ const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { - const { location } = mark; + const { location } = mark - ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE; - ctx!.fillStyle = 'black'; - const textMetrics = ctx!.measureText(mark.value!); - 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); + ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE + ctx!.fillStyle = 'black' + const textMetrics = ctx!.measureText(mark.value!) + 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) } /** @@ -188,7 +192,7 @@ const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { * @param markedPdfPages */ const convertToPdfBlob = async (markedPdfPages: string[]): Promise => { - const pdfDoc = await PDFDocument.create(); + const pdfDoc = await PDFDocument.create() for (const page of markedPdfPages) { const pngImage = await pdfDoc.embedPng(page) @@ -203,7 +207,6 @@ const convertToPdfBlob = async (markedPdfPages: string[]): Promise => { const pdfBytes = await pdfDoc.save() return new Blob([pdfBytes], { type: 'application/pdf' }) - } /** @@ -211,9 +214,12 @@ const convertToPdfBlob = async (markedPdfPages: string[]): Promise => { * @param arrayBuffer * @param fileName */ -const convertToPdfFile = async (arrayBuffer: ArrayBuffer, fileName: string): Promise => { - const file = toFile(arrayBuffer, fileName); - return toPdfFile(file); +const convertToPdfFile = async ( + arrayBuffer: ArrayBuffer, + fileName: string +): Promise => { + const file = toFile(arrayBuffer, fileName) + return toPdfFile(file) } /** @@ -226,7 +232,7 @@ const groupMarksByPage = (marks: Mark[]) => { return marks .filter(hasValue) .map(scaleMark) - .reduce<{[key: number]: Mark[]}>(byPage, {}) + .reduce<{ [key: number]: Mark[] }>(byPage, {}) } /** @@ -237,11 +243,10 @@ const groupMarksByPage = (marks: Mark[]) => { * @param obj - accumulator in the reducer callback * @param mark - current value, i.e. Mark being examined */ -const byPage = (obj: { [key: number]: Mark[]}, mark: Mark) => { - const key = mark.location.page; - const curGroup = obj[key] ?? []; - return { ...obj, [key]: [...curGroup, mark] - } +const byPage = (obj: { [key: number]: Mark[] }, mark: Mark) => { + const key = mark.location.page + const curGroup = obj[key] ?? [] + return { ...obj, [key]: [...curGroup, mark] } } export { @@ -252,5 +257,5 @@ export { convertToPdfFile, addMarks, convertToPdfBlob, - groupMarksByPage, -} \ No newline at end of file + groupMarksByPage +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ed830a2..e201c41 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,4 +1,5 @@ import { PdfFile } from '../types/drawing.ts' +import { CurrentUserFile } from '../types/file.ts' export const compareObjects = ( obj1: object | null | undefined, @@ -72,11 +73,16 @@ export const timeout = (ms: number = 60000) => { * @param files * @param fileHashes */ -export const getFilesWithHashes = ( - files: { [filename: string ]: PdfFile }, +export const getCurrentUserFiles = ( + files: { [filename: string]: PdfFile }, fileHashes: { [key: string]: string | null } - ) => { - return Object.entries(files).map(([filename, pdfFile]) => { - return { pdfFile, filename, hash: fileHashes[filename] } +): CurrentUserFile[] => { + return Object.entries(files).map(([filename, pdfFile], index) => { + return { + pdfFile, + filename, + id: index + 1, + ...(!!fileHashes[filename] && { hash: fileHashes[filename]! }) + } }) } From d9779c10bde5a510f2ac8daf660857299dc585e9 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 13 Aug 2024 13:32:32 +0200 Subject: [PATCH 06/63] refactor: flat meta and add useSigitProfile --- src/components/DisplaySigit/index.tsx | 65 ++-------------- src/hooks/useSigitMeta.tsx | 70 ++++++++++++++--- src/hooks/useSigitProfiles.tsx | 70 +++++++++++++++++ src/pages/verify/index.tsx | 107 +++++--------------------- src/utils/meta.ts | 4 +- 5 files changed, 155 insertions(+), 161 deletions(-) create mode 100644 src/hooks/useSigitProfiles.tsx diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index 0fe5c2f..ce82f47 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -1,9 +1,6 @@ -import { useEffect, useState } from 'react' -import { Meta, ProfileMetadata } from '../../types' +import { Meta } from '../../types' import { SigitCardDisplayInfo, SigitStatus } from '../../utils' -import { Event, kinds } from 'nostr-tools' import { Link } from 'react-router-dom' -import { MetadataController } from '../../controllers' import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils' import { appPublicRoutes, appPrivateRoutes } from '../../routes' import { Button, Divider, Tooltip } from '@mui/material' @@ -22,6 +19,7 @@ import { UserAvatarGroup } from '../UserAvatarGroup' import styles from './style.module.scss' import { TooltipChild } from '../TooltipChild' import { getExtensionIconLabel } from '../getExtensionIconLabel' +import { useSigitProfiles } from '../../hooks/useSigitProfiles' type SigitProps = { meta: Meta @@ -38,61 +36,10 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { fileExtensions } = parsedMeta - const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>( - {} - ) - - useEffect(() => { - const hexKeys = new Set([ - ...signers.map((signer) => npubToHex(signer)!) - ]) - - if (submittedBy) { - hexKeys.add(npubToHex(submittedBy)!) - } - - const metadataController = new MetadataController() - - const handleMetadataEvent = (key: string) => (event: Event) => { - const metadataContent = - metadataController.extractProfileMetadataContent(event) - - if (metadataContent) { - setProfiles((prev) => ({ - ...prev, - [key]: metadataContent - })) - } - } - - const handleEventListener = - (key: string) => (kind: number, event: Event) => { - if (kind === kinds.Metadata) { - handleMetadataEvent(key)(event) - } - } - - hexKeys.forEach((key) => { - if (!(key in profiles)) { - metadataController.on(key, handleEventListener(key)) - - metadataController - .findMetadata(key) - .then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(key)(metadataEvent) - }) - .catch((err) => { - console.error(`error occurred in finding metadata for: ${key}`, err) - }) - } - }) - - return () => { - hexKeys.forEach((key) => { - metadataController.off(key, handleEventListener(key)) - }) - } - }, [submittedBy, signers, profiles]) + const profiles = useSigitProfiles([ + ...(submittedBy ? [submittedBy] : []), + ...signers + ]) return (
diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index aebd791..5460983 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -3,6 +3,7 @@ import { CreateSignatureEventContent, Meta } from '../types' import { Mark } from '../types/mark' import { fromUnixTimestamp, + hexToNpub, parseCreateSignatureEvent, parseCreateSignatureEventContent, SigitMetaParseError, @@ -12,11 +13,30 @@ import { import { toast } from 'react-toastify' import { verifyEvent } from 'nostr-tools' import { Event } from 'nostr-tools' +import store from '../store/store' +import { AuthState } from '../store/auth/types' +import { NostrController } from '../controllers' + +/** + * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`, + * and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions) + */ +interface FlatMeta + extends Meta, + CreateSignatureEventContent, + Partial> { + // Remove pubkey and use submittedBy as `npub1${string}` + submittedBy?: `npub1${string}` + + // Remove created_at and replace with createdAt + createdAt?: number -interface FlatMeta extends Meta, CreateSignatureEventContent, Partial { // Validated create signature event isValid: boolean + // Decryption + encryptionKey: string | null + // Calculated status fields signedStatus: SigitStatus signersStatus: { @@ -33,8 +53,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { const [isValid, setIsValid] = useState(false) const [kind, setKind] = useState() const [tags, setTags] = useState() - const [created_at, setCreatedAt] = useState() - const [pubkey, setPubkey] = useState() // submittedBy, pubkey from nostr event + const [createdAt, setCreatedAt] = useState() + const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event const [id, setId] = useState() const [sig, setSig] = useState() @@ -54,6 +74,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { [signer: `npub1${string}`]: SignStatus }>({}) + const [encryptionKey, setEncryptionKey] = useState(null) + useEffect(() => { if (!meta) return ;(async function () { @@ -70,7 +92,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { setTags(tags) // created_at in nostr events are stored in seconds setCreatedAt(fromUnixTimestamp(created_at)) - setPubkey(pubkey) + setSubmittedBy(pubkey as `npub1${string}`) setId(id) setSig(sig) @@ -84,6 +106,31 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { setMarkConfig(markConfig) setZipUrl(zipUrl) + if (meta.keys) { + const { sender, keys } = meta.keys + + // Retrieve the user's public key from the state + const usersPubkey = (store.getState().auth as AuthState).usersPubkey! + const usersNpub = hexToNpub(usersPubkey) + + // Check if the user's public key is in the keys object + if (usersNpub in keys) { + // Instantiate the NostrController to decrypt the encryption key + const nostrController = NostrController.getInstance() + const decrypted = await nostrController + .nip04Decrypt(sender, keys[usersNpub]) + .catch((err) => { + console.log( + 'An error occurred in decrypting encryption key', + err + ) + return null + }) + + setEncryptionKey(decrypted) + } + } + // Parse each signature event and set signer status for (const npub in meta.docSignatures) { try { @@ -125,15 +172,15 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { }, [meta]) return { - modifiedAt: meta.modifiedAt, - createSignature: meta.createSignature, - docSignatures: meta.docSignatures, - keys: meta.keys, + modifiedAt: meta?.modifiedAt, + createSignature: meta?.createSignature, + docSignatures: meta?.docSignatures, + keys: meta?.keys, isValid, kind, tags, - created_at, - pubkey, + createdAt, + submittedBy, id, sig, signers, @@ -143,6 +190,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { title, zipUrl, signedStatus, - signersStatus + signersStatus, + encryptionKey } } diff --git a/src/hooks/useSigitProfiles.tsx b/src/hooks/useSigitProfiles.tsx new file mode 100644 index 0000000..8178dd7 --- /dev/null +++ b/src/hooks/useSigitProfiles.tsx @@ -0,0 +1,70 @@ +import { useEffect, useState } from 'react' +import { ProfileMetadata } from '../types' +import { MetadataController } from '../controllers' +import { npubToHex } from '../utils' +import { Event, kinds } from 'nostr-tools' + +/** + * Extracts profiles from metadata events + * @param pubkeys Array of npubs to check + * @returns ProfileMetadata + */ +export const useSigitProfiles = ( + pubkeys: `npub1${string}`[] +): { [key: string]: ProfileMetadata } => { + const [profileMetadata, setProfileMetadata] = useState<{ + [key: string]: ProfileMetadata + }>({}) + + useEffect(() => { + if (pubkeys.length) { + const metadataController = new MetadataController() + + // Remove duplicate keys + const users = new Set([...pubkeys]) + + const handleMetadataEvent = (key: string) => (event: Event) => { + const metadataContent = + metadataController.extractProfileMetadataContent(event) + + if (metadataContent) { + setProfileMetadata((prev) => ({ + ...prev, + [key]: metadataContent + })) + } + } + + users.forEach((user) => { + const hexKey = npubToHex(user) + if (hexKey && !(hexKey in profileMetadata)) { + metadataController.on(hexKey, (kind: number, event: Event) => { + if (kind === kinds.Metadata) { + handleMetadataEvent(hexKey)(event) + } + }) + + metadataController + .findMetadata(hexKey) + .then((metadataEvent) => { + if (metadataEvent) handleMetadataEvent(hexKey)(metadataEvent) + }) + .catch((err) => { + console.error( + `error occurred in finding metadata for: ${user}`, + err + ) + }) + } + }) + + return () => { + users.forEach((key) => { + metadataController.off(key, handleMetadataEvent(key)) + }) + } + } + }, [pubkeys, profileMetadata]) + + return profileMetadata +} diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 1f6bee1..f77bd48 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -10,22 +10,20 @@ import { } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' -import { Event, kinds, verifyEvent } from 'nostr-tools' +import { Event, verifyEvent } from 'nostr-tools' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { UserAvatar } from '../../components/UserAvatar' -import { MetadataController, NostrController } from '../../controllers' +import { NostrController } from '../../controllers' import { CreateSignatureEventContent, Meta, - ProfileMetadata, SignedEventContent } from '../../types' import { decryptArrayBuffer, extractMarksFromSignedMeta, - extractZipUrlAndEncryptionKey, getHash, hexToNpub, unixNow, @@ -51,6 +49,8 @@ import { useSelector } from 'react-redux' import { getLastSignersSig } from '../../utils/sign.ts' import { saveAs } from 'file-saver' import { Container } from '../../components/Container' +import { useSigitMeta } from '../../hooks/useSigitMeta.tsx' +import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx' export const VerifyPage = () => { const theme = useTheme() @@ -63,52 +63,35 @@ export const VerifyPage = () => { * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json * meta will be received in navigation from create & home page in online mode */ - const { uploadedZip, meta: metaInNavState } = location.state || {} + const { uploadedZip, meta } = location.state || {} + + const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } = + useSigitMeta(meta) + const profiles = useSigitProfiles([ + ...(submittedBy ? [submittedBy] : []), + ...signers, + ...viewers + ]) const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [selectedFile, setSelectedFile] = useState(null) - const [meta, setMeta] = useState(null) - const [submittedBy, setSubmittedBy] = useState() - - const [signers, setSigners] = useState<`npub1${string}`[]>([]) - const [viewers, setViewers] = useState<`npub1${string}`[]>([]) - const [creatorFileHashes, setCreatorFileHashes] = useState<{ - [key: string]: string - }>({}) const [currentFileHashes, setCurrentFileHashes] = useState<{ [key: string]: string | null - }>({}) + }>(fileHashes) const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) - const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( - {} - ) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const nostrController = NostrController.getInstance() useEffect(() => { if (uploadedZip) { setSelectedFile(uploadedZip) - } else if (metaInNavState) { + } else if (meta && encryptionKey) { const processSigit = async () => { setIsLoading(true) - setLoadingSpinnerDesc('Extracting zipUrl and encryption key from meta') - - const res = await extractZipUrlAndEncryptionKey(metaInNavState) - if (!res) { - setIsLoading(false) - return - } - - const { - zipUrl, - encryptionKey, - createSignatureEvent, - createSignatureContent - } = res setLoadingSpinnerDesc('Fetching file from file server') axios @@ -175,12 +158,6 @@ export const VerifyPage = () => { setCurrentFileHashes(fileHashes) setFiles(files) - setSigners(createSignatureContent.signers) - setViewers(createSignatureContent.viewers) - setCreatorFileHashes(createSignatureContent.fileHashes) - setSubmittedBy(createSignatureEvent.pubkey) - - setMeta(metaInNavState) setIsLoading(false) } }) @@ -197,49 +174,7 @@ export const VerifyPage = () => { processSigit() } - }, [uploadedZip, metaInNavState]) - - useEffect(() => { - if (submittedBy) { - const metadataController = new MetadataController() - - const users = [submittedBy, ...signers, ...viewers] - - users.forEach((user) => { - const pubkey = npubToHex(user)! - - if (!(pubkey in metadata)) { - const handleMetadataEvent = (event: Event) => { - const metadataContent = - metadataController.extractProfileMetadataContent(event) - if (metadataContent) - setMetadata((prev) => ({ - ...prev, - [pubkey]: metadataContent - })) - } - - metadataController.on(pubkey, (kind: number, event: Event) => { - if (kind === kinds.Metadata) { - handleMetadataEvent(event) - } - }) - - metadataController - .findMetadata(pubkey) - .then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(metadataEvent) - }) - .catch((err) => { - console.error( - `error occurred in finding metadata for: ${user}`, - err - ) - }) - } - }) - } - }, [submittedBy, signers, viewers, metadata]) + }, [encryptionKey, meta, uploadedZip, zipUrl]) const handleVerify = async () => { if (!selectedFile) return @@ -345,12 +280,6 @@ export const VerifyPage = () => { if (!createSignatureContent) return - setSigners(createSignatureContent.signers) - setViewers(createSignatureContent.viewers) - setCreatorFileHashes(createSignatureContent.fileHashes) - setSubmittedBy(createSignatureEvent.pubkey) - - setMeta(parsedMetaJson) setIsLoading(false) } @@ -451,7 +380,7 @@ export const VerifyPage = () => { } const displayUser = (pubkey: string, verifySignature = false) => { - const profile = metadata[pubkey] + const profile = profiles[pubkey] let isValidSignature = false @@ -682,7 +611,7 @@ export const VerifyPage = () => { {Object.entries(currentFileHashes).map( ([filename, hash], index) => { - const isValidHash = creatorFileHashes[filename] === hash + const isValidHash = fileHashes[filename] === hash return ( diff --git a/src/utils/meta.ts b/src/utils/meta.ts index b3c0c28..74d38b7 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -69,7 +69,7 @@ export enum SigitMetaParseErrorType { export interface SigitCardDisplayInfo { createdAt?: number title?: string - submittedBy?: string + submittedBy?: `npub1${string}` signers: `npub1${string}`[] fileExtensions: string[] signedStatus: SigitStatus @@ -161,7 +161,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { ) sigitInfo.title = createSignatureContent.title - sigitInfo.submittedBy = createSignatureEvent.pubkey + sigitInfo.submittedBy = createSignatureEvent.pubkey as `npub1${string}` sigitInfo.signers = createSignatureContent.signers sigitInfo.fileExtensions = extensions From bc23361fb037779de5d7c6fe89cdbefd2ce0479d Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 13 Aug 2024 17:26:21 +0200 Subject: [PATCH 07/63] refactor: move styles from page to Avatar Group component --- src/components/DisplaySigit/index.tsx | 2 +- src/components/DisplaySigit/style.module.scss | 20 ------------------- src/components/UserAvatarGroup/index.tsx | 2 +- .../UserAvatarGroup/style.module.scss | 20 +++++++++++++++++++ 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index ce82f47..dfbcbba 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -77,7 +77,7 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { {submittedBy && signers.length ? ( ) : null} - + {signers.map((signer) => { const pubkey = npubToHex(signer)! const profile = profiles[pubkey] diff --git a/src/components/DisplaySigit/style.module.scss b/src/components/DisplaySigit/style.module.scss index 7544fc4..4bb2f15 100644 --- a/src/components/DisplaySigit/style.module.scss +++ b/src/components/DisplaySigit/style.module.scss @@ -93,26 +93,6 @@ grid-gap: 10px; } -.signers { - padding: 0 0 0 10px; - - > * { - transition: margin ease 0.2s; - margin: 0 0 0 -10px; - position: relative; - z-index: 1; - &:first-child { - margin-left: -10px !important; - } - } - - > *:hover, - > *:focus-within { - margin: 0 15px 0 5px; - z-index: 2; - } -} - .details { color: rgba(0, 0, 0, 0.3); font-size: 14px; diff --git a/src/components/UserAvatarGroup/index.tsx b/src/components/UserAvatarGroup/index.tsx index 13f8b25..f8e231f 100644 --- a/src/components/UserAvatarGroup/index.tsx +++ b/src/components/UserAvatarGroup/index.tsx @@ -28,7 +28,7 @@ export const UserAvatarGroup = ({ const childrenArray = Children.toArray(children) return ( -
+
{surplus > 1 ? childrenArray.slice(0, surplus * -1).map((c) => c) : children} diff --git a/src/components/UserAvatarGroup/style.module.scss b/src/components/UserAvatarGroup/style.module.scss index 9604202..c9ee551 100644 --- a/src/components/UserAvatarGroup/style.module.scss +++ b/src/components/UserAvatarGroup/style.module.scss @@ -1,5 +1,25 @@ @import '../../styles/colors.scss'; +.container { + padding: 0 0 0 10px; + + > * { + transition: margin ease 0.2s; + margin: 0 0 0 -10px; + position: relative; + z-index: 1; + &:first-child { + margin-left: -10px !important; + } + } + + > *:hover, + > *:focus-within { + margin: 0 15px 0 5px; + z-index: 2; + } +} + .icon { width: 40px; height: 40px; From e16b8cfe3fe297983a3fd122ac25b89ca1568835 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 13 Aug 2024 17:27:08 +0200 Subject: [PATCH 08/63] feat: add sticky layout with slots --- src/layouts/Files.module.scss | 24 ++++++++++++++++++++++++ src/layouts/Files.tsx | 29 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/layouts/Files.module.scss create mode 100644 src/layouts/Files.tsx diff --git a/src/layouts/Files.module.scss b/src/layouts/Files.module.scss new file mode 100644 index 0000000..bda18dc --- /dev/null +++ b/src/layouts/Files.module.scss @@ -0,0 +1,24 @@ +@import '../styles/colors.scss'; +@import '../styles/sizes.scss'; + +.container { + display: grid; + grid-template-columns: 0.75fr 1.5fr 0.75fr; + grid-gap: 30px; + flex-grow: 1; +} + +.sidesWrap { + position: relative; +} + +.sides { + position: sticky; + top: $header-height + $body-vertical-padding; +} + +.files { + display: flex; + flex-direction: column; + grid-gap: 15px; +} diff --git a/src/layouts/Files.tsx b/src/layouts/Files.tsx new file mode 100644 index 0000000..a494293 --- /dev/null +++ b/src/layouts/Files.tsx @@ -0,0 +1,29 @@ +import { PropsWithChildren, ReactNode } from 'react' + +import styles from './Files.module.scss' + +interface FilesProps { + left: ReactNode + right: ReactNode + content: ReactNode +} + +export const Files = ({ + left, + right, + content, + children +}: PropsWithChildren) => { + return ( +
+
+
{left}
+
+
{content}
+
+
{right}
+
+ {children} +
+ ) +} From b3155cce0d9cc6be4fc20c9130988c76ea417784 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 13 Aug 2024 17:28:14 +0200 Subject: [PATCH 09/63] refactor: expand useSigitMeta and update verify w details section --- src/components/FilesUsers.tsx/index.tsx | 246 +++++++++++++++++ .../FilesUsers.tsx/style.module.scss | 41 +++ src/hooks/useSigitMeta.tsx | 54 +++- src/pages/verify/index.tsx | 249 ++---------------- src/utils/meta.ts | 47 ++-- 5 files changed, 376 insertions(+), 261 deletions(-) create mode 100644 src/components/FilesUsers.tsx/index.tsx create mode 100644 src/components/FilesUsers.tsx/style.module.scss diff --git a/src/components/FilesUsers.tsx/index.tsx b/src/components/FilesUsers.tsx/index.tsx new file mode 100644 index 0000000..59e3a09 --- /dev/null +++ b/src/components/FilesUsers.tsx/index.tsx @@ -0,0 +1,246 @@ +import { CheckCircle, Cancel } from '@mui/icons-material' +import { Divider, Tooltip } from '@mui/material' +import { verifyEvent } from 'nostr-tools' +import { useSigitProfiles } from '../../hooks/useSigitProfiles' +import { Meta, SignedEventContent } from '../../types' +import { + extractFileExtensions, + formatTimestamp, + fromUnixTimestamp, + hexToNpub, + npubToHex, + shorten +} from '../../utils' +import { UserAvatar } from '../UserAvatar' +import { useSigitMeta } from '../../hooks/useSigitMeta' +import { UserAvatarGroup } from '../UserAvatarGroup' + +import styles from './style.module.scss' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { + faCalendar, + faCalendarCheck, + faCalendarPlus, + faEye, + faFile, + faFileCircleExclamation +} from '@fortawesome/free-solid-svg-icons' +import { getExtensionIconLabel } from '../getExtensionIconLabel' +import { useSelector } from 'react-redux' +import { State } from '../../store/rootReducer' + +interface FileUsersProps { + meta: Meta +} + +export const FileUsers = ({ meta }: FileUsersProps) => { + const { usersPubkey } = useSelector((state: State) => state.auth) + const { + submittedBy, + signers, + viewers, + fileHashes, + sig, + docSignatures, + parsedSignatureEvents, + createdAt, + signedStatus, + completedAt + } = useSigitMeta(meta) + const profiles = useSigitProfiles([ + ...(submittedBy ? [submittedBy] : []), + ...signers, + ...viewers + ]) + const userCanSign = + typeof usersPubkey !== 'undefined' && + signers.includes(hexToNpub(usersPubkey)) + + const ext = extractFileExtensions(Object.keys(fileHashes)) + + const getPrevSignersSig = (npub: string) => { + // if user is first signer then use creator's signature + if (signers[0] === npub) { + return sig + } + + // find the index of signer + const currentSignerIndex = signers.findIndex((signer) => signer === npub) + // return null if could not found user in signer's list + if (currentSignerIndex === -1) return null + // find prev signer + const prevSigner = signers[currentSignerIndex - 1] + + // get the signature of prev signer + try { + const prevSignersEvent = parsedSignatureEvents[prevSigner] + return prevSignersEvent.sig + } catch (error) { + return null + } + } + + const displayUser = (pubkey: string, verifySignature = false) => { + const profile = profiles[pubkey] + + let isValidSignature = false + + if (verifySignature) { + const npub = hexToNpub(pubkey) + const signedEventString = docSignatures[npub] + if (signedEventString) { + try { + const signedEvent = JSON.parse(signedEventString) + const isVerifiedEvent = verifyEvent(signedEvent) + + if (isVerifiedEvent) { + // get the actual signature of prev signer + const prevSignersSig = getPrevSignersSig(npub) + + // get the signature of prev signer from the content of current signers signedEvent + + try { + const obj: SignedEventContent = JSON.parse(signedEvent.content) + if ( + obj.prevSig && + prevSignersSig && + obj.prevSig === prevSignersSig + ) { + isValidSignature = true + } + } catch (error) { + isValidSignature = false + } + } + } catch (error) { + console.error( + `An error occurred in parsing and verifying the signature event for ${pubkey}`, + error + ) + } + } + } + + return ( + <> + + + {verifySignature && ( + <> + {isValidSignature && ( + + + + )} + + {!isValidSignature && ( + + + + )} + + )} + + ) + } + + return submittedBy ? ( +
+
+

Signers

+ {displayUser(submittedBy)} + {submittedBy && signers.length ? ( + + ) : null} + + {signers.length > 0 && + signers.map((signer) => ( + {displayUser(npubToHex(signer)!, true)} + ))} + {viewers.length > 0 && + viewers.map((viewer) => ( + {displayUser(npubToHex(viewer)!)} + ))} + +
+
+

Details

+ + + + {' '} + {createdAt ? formatTimestamp(createdAt) : <>—} + + + + + + {' '} + {completedAt ? formatTimestamp(completedAt) : <>—} + + + + {/* User signed date */} + {userCanSign ? ( + + + {' '} + {hexToNpub(usersPubkey) in parsedSignatureEvents ? ( + parsedSignatureEvents[hexToNpub(usersPubkey)].created_at ? ( + formatTimestamp( + fromUnixTimestamp( + parsedSignatureEvents[hexToNpub(usersPubkey)].created_at + ) + ) + ) : ( + <>— + ) + ) : ( + <>— + )} + + + ) : null} + + {signedStatus} + + {ext.length > 0 ? ( + + {ext.length > 1 ? ( + <> + Multiple File Types + + ) : ( + getExtensionIconLabel(ext[0]) + )} + + ) : ( + <> + — + + )} +
+
+ ) : undefined +} diff --git a/src/components/FilesUsers.tsx/style.module.scss b/src/components/FilesUsers.tsx/style.module.scss new file mode 100644 index 0000000..b6e0313 --- /dev/null +++ b/src/components/FilesUsers.tsx/style.module.scss @@ -0,0 +1,41 @@ +@import '../../styles/colors.scss'; + +.container { + border-radius: 4px; + background: $overlay-background-color; + padding: 15px; + display: flex; + flex-direction: column; + grid-gap: 25px; + + font-size: 14px; +} + +.section { + display: flex; + flex-direction: column; + grid-gap: 10px; +} + +.detailsItem { + transition: ease 0.2s; + color: rgba(0, 0, 0, 0.5); + font-size: 14px; + align-items: center; + border-radius: 4px; + padding: 5px; + + display: flex; + align-items: center; + justify-content: start; + + > :first-child { + padding: 5px; + margin-right: 10px; + } + + &:hover { + background: $primary-main; + color: white; + } +} diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index 5460983..78516d5 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -4,7 +4,7 @@ import { Mark } from '../types/mark' import { fromUnixTimestamp, hexToNpub, - parseCreateSignatureEvent, + parseNostrEvent, parseCreateSignatureEventContent, SigitMetaParseError, SigitStatus, @@ -21,7 +21,7 @@ import { NostrController } from '../controllers' * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`, * and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions) */ -interface FlatMeta +export interface FlatMeta extends Meta, CreateSignatureEventContent, Partial> { @@ -37,6 +37,12 @@ interface FlatMeta // Decryption encryptionKey: string | null + // Parsed Document Signatures + parsedSignatureEvents: { [signer: `npub1${string}`]: Event } + + // Calculated completion time + completedAt?: number + // Calculated status fields signedStatus: SigitStatus signersStatus: { @@ -67,6 +73,12 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { const [title, setTitle] = useState('') const [zipUrl, setZipUrl] = useState('') + const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{ + [signer: `npub1${string}`]: Event + }>({}) + + const [completedAt, setCompletedAt] = useState() + const [signedStatus, setSignedStatus] = useState( SigitStatus.Partial ) @@ -80,9 +92,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { if (!meta) return ;(async function () { try { - const createSignatureEvent = await parseCreateSignatureEvent( - meta.createSignature - ) + const createSignatureEvent = await parseNostrEvent(meta.createSignature) const { kind, tags, created_at, pubkey, id, sig, content } = createSignatureEvent @@ -131,13 +141,22 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { } } - // Parse each signature event and set signer status + // Temp. map to hold events + const parsedSignatureEventsMap = new Map<`npub1${string}`, Event>() for (const npub in meta.docSignatures) { try { - const event = await parseCreateSignatureEvent( + // Parse each signature event + const event = await parseNostrEvent( meta.docSignatures[npub as `npub1${string}`] ) + const isValidSignature = verifyEvent(event) + + // Save events to a map, to save all at once outside loop + // We need the object to find completedAt + // Avoided using parsedSignatureEvents due to useEffect deps + parsedSignatureEventsMap.set(npub as `npub1${string}`, event) + setSignersStatus((prev) => { return { ...prev, @@ -155,6 +174,11 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { }) } } + + setParsedSignatureEvents( + Object.fromEntries(parsedSignatureEventsMap.entries()) + ) + const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] const isCompletelySigned = signers.every((signer) => signedBy.includes(signer) @@ -162,6 +186,20 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { setSignedStatus( isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial ) + + // Check if all signers signed and are valid + if (isCompletelySigned) { + setCompletedAt( + fromUnixTimestamp( + signedBy.reduce((p, c) => { + return Math.max( + p, + parsedSignatureEventsMap.get(c)?.created_at || 0 + ) + }, 0) + ) + ) + } } catch (error) { if (error instanceof SigitMetaParseError) { toast.error(error.message) @@ -189,6 +227,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { markConfig, title, zipUrl, + parsedSignatureEvents, + completedAt, signedStatus, signersStatus, encryptionKey diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index f77bd48..d6a297d 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -1,36 +1,20 @@ -import { - Box, - Button, - List, - ListItem, - ListSubheader, - Tooltip, - Typography, - useTheme -} from '@mui/material' +import { Box, Button, Tooltip, Typography, useTheme } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' import { Event, verifyEvent } from 'nostr-tools' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' -import { UserAvatar } from '../../components/UserAvatar' import { NostrController } from '../../controllers' -import { - CreateSignatureEventContent, - Meta, - SignedEventContent -} from '../../types' +import { CreateSignatureEventContent, Meta } from '../../types' import { decryptArrayBuffer, extractMarksFromSignedMeta, getHash, hexToNpub, unixNow, - npubToHex, parseJson, readContentOfZipEntry, - shorten, signEventForMetaFile } from '../../utils' import styles from './style.module.scss' @@ -50,7 +34,8 @@ import { getLastSignersSig } from '../../utils/sign.ts' import { saveAs } from 'file-saver' import { Container } from '../../components/Container' import { useSigitMeta } from '../../hooks/useSigitMeta.tsx' -import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx' +import { Files } from '../../layouts/Files.tsx' +import { FileUsers } from '../../components/FilesUsers.tsx/index.tsx' export const VerifyPage = () => { const theme = useTheme() @@ -67,11 +52,6 @@ export const VerifyPage = () => { const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } = useSigitMeta(meta) - const profiles = useSigitProfiles([ - ...(submittedBy ? [submittedBy] : []), - ...signers, - ...viewers - ]) const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -283,35 +263,6 @@ export const VerifyPage = () => { setIsLoading(false) } - const getPrevSignersSig = (npub: string) => { - if (!meta) return null - - // if user is first signer then use creator's signature - if (signers[0] === npub) { - try { - const createSignatureEvent: Event = JSON.parse(meta.createSignature) - return createSignatureEvent.sig - } catch (error) { - return null - } - } - - // find the index of signer - const currentSignerIndex = signers.findIndex((signer) => signer === npub) - // return null if could not found user in signer's list - if (currentSignerIndex === -1) return null - // find prev signer - const prevSigner = signers[currentSignerIndex - 1] - - // get the signature of prev signer - try { - const prevSignersEvent: Event = JSON.parse(meta.docSignatures[prevSigner]) - return prevSignersEvent.sig - } catch (error) { - return null - } - } - const handleExport = async () => { if (Object.entries(files).length === 0 || !meta || !usersPubkey) return @@ -379,76 +330,6 @@ export const VerifyPage = () => { setIsLoading(false) } - const displayUser = (pubkey: string, verifySignature = false) => { - const profile = profiles[pubkey] - - let isValidSignature = false - - if (verifySignature) { - const npub = hexToNpub(pubkey) - const signedEventString = meta ? meta.docSignatures[npub] : null - if (signedEventString) { - try { - const signedEvent = JSON.parse(signedEventString) - const isVerifiedEvent = verifyEvent(signedEvent) - - if (isVerifiedEvent) { - // get the actual signature of prev signer - const prevSignersSig = getPrevSignersSig(npub) - - // get the signature of prev signer from the content of current signers signedEvent - - try { - const obj: SignedEventContent = JSON.parse(signedEvent.content) - if ( - obj.prevSig && - prevSignersSig && - obj.prevSig === prevSignersSig - ) { - isValidSignature = true - } - } catch (error) { - isValidSignature = false - } - } - } catch (error) { - console.error( - `An error occurred in parsing and verifying the signature event for ${pubkey}`, - error - ) - } - } - } - - return ( - <> - - - {verifySignature && ( - <> - {isValidSignature && ( - - - - )} - - {!isValidSignature && ( - - - - )} - - )} - - ) - } - const displayExportedBy = () => { if (!meta || !meta.exportSignature) return null @@ -458,7 +339,7 @@ export const VerifyPage = () => { const exportSignatureEvent = JSON.parse(exportSignatureString) as Event if (verifyEvent(exportSignatureEvent)) { - return displayUser(exportSignatureEvent.pubkey) + // return displayUser(exportSignatureEvent.pubkey) } else { toast.error(`Invalid export signature!`) return ( @@ -505,109 +386,9 @@ export const VerifyPage = () => { )} {meta && ( - <> - - Meta Info - - } - > - {submittedBy && ( - - - Submitted By - - {displayUser(submittedBy)} - - )} - - - - Exported By - - {displayExportedBy()} - - - - - - {signers.length > 0 && ( - - - Signers - -
    - {signers.map((signer) => ( -
  • - {displayUser(npubToHex(signer)!, true)} -
  • - ))} -
-
- )} - - {viewers.length > 0 && ( - - - Viewers - -
    - {viewers.map((viewer) => ( -
  • - {displayUser(npubToHex(viewer)!)} -
  • - ))} -
-
- )} - - - - Files - + {Object.entries(currentFileHashes).map( ([filename, hash], index) => { @@ -643,9 +424,17 @@ export const VerifyPage = () => { } )} - -
- + {displayExportedBy()} + + + + + } + right={} + content={
} + /> )} diff --git a/src/utils/meta.ts b/src/utils/meta.ts index 74d38b7..dd29b60 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -62,7 +62,7 @@ function handleError(error: unknown): Error { // Reuse common error messages for meta parsing export enum SigitMetaParseErrorType { - 'PARSE_ERROR_SIGNATURE_EVENT' = 'error occurred in parsing the create signature event', + 'PARSE_ERROR_EVENT' = 'error occurred in parsing the create signature event', 'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content" } @@ -76,24 +76,19 @@ export interface SigitCardDisplayInfo { } /** - * Wrapper for createSignatureEvent parse that throws custom SigitMetaParseError with cause and context + * Wrapper for event parser that throws custom SigitMetaParseError with cause and context * @param raw Raw string for parsing * @returns parsed Event */ -export const parseCreateSignatureEvent = async ( - raw: string -): Promise => { +export const parseNostrEvent = async (raw: string): Promise => { try { - const createSignatureEvent = await parseJson(raw) - return createSignatureEvent + const event = await parseJson(raw) + return event } catch (error) { - throw new SigitMetaParseError( - SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT, - { - cause: handleError(error), - context: raw - } - ) + throw new SigitMetaParseError(SigitMetaParseErrorType.PARSE_ERROR_EVENT, { + cause: handleError(error), + context: raw + }) } } @@ -135,9 +130,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { } try { - const createSignatureEvent = await parseCreateSignatureEvent( - meta.createSignature - ) + const createSignatureEvent = await parseNostrEvent(meta.createSignature) // created_at in nostr events are stored in seconds sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at) @@ -147,13 +140,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { ) const files = Object.keys(createSignatureContent.fileHashes) - const extensions = files.reduce((result: string[], file: string) => { - const extension = file.split('.').pop() - if (extension) { - result.push(extension) - } - return result - }, []) + const extensions = extractFileExtensions(files) const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] const isCompletelySigned = createSignatureContent.signers.every((signer) => @@ -179,3 +166,15 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { } } } + +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 + }, []) + + return extensions +} From 5ffedb68d67a1a60177861d0897341ab10741558 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 11:20:48 +0200 Subject: [PATCH 10/63] refactor: rename Files to StickySideColumns and update css --- ...odule.scss => StickySideColumns.module.scss} | 5 +++++ .../{Files.tsx => StickySideColumns.tsx} | 17 +++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) rename src/layouts/{Files.module.scss => StickySideColumns.module.scss} (84%) rename src/layouts/{Files.tsx => StickySideColumns.tsx} (58%) diff --git a/src/layouts/Files.module.scss b/src/layouts/StickySideColumns.module.scss similarity index 84% rename from src/layouts/Files.module.scss rename to src/layouts/StickySideColumns.module.scss index bda18dc..63c4314 100644 --- a/src/layouts/Files.module.scss +++ b/src/layouts/StickySideColumns.module.scss @@ -22,3 +22,8 @@ flex-direction: column; grid-gap: 15px; } +.content { + max-width: 550px; + width: 100%; + margin: 0 auto; +} diff --git a/src/layouts/Files.tsx b/src/layouts/StickySideColumns.tsx similarity index 58% rename from src/layouts/Files.tsx rename to src/layouts/StickySideColumns.tsx index a494293..1ada87f 100644 --- a/src/layouts/Files.tsx +++ b/src/layouts/StickySideColumns.tsx @@ -1,29 +1,26 @@ import { PropsWithChildren, ReactNode } from 'react' -import styles from './Files.module.scss' +import styles from './StickySideColumns.module.scss' -interface FilesProps { - left: ReactNode - right: ReactNode - content: ReactNode +interface StickySideColumnsProps { + left?: ReactNode + right?: ReactNode } -export const Files = ({ +export const StickySideColumns = ({ left, right, - content, children -}: PropsWithChildren) => { +}: PropsWithChildren) => { return (
{left}
-
{content}
+
{children}
{right}
- {children}
) } From 97c82718cb2991309894d2fd823f25035504b9be Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 14 Aug 2024 12:24:15 +0300 Subject: [PATCH 11/63] fix: page scrolling --- src/components/FileList/style.module.scss | 10 ++-- src/components/PDFView/PdfItem.tsx | 44 ++++++++++------- src/components/PDFView/PdfMarking.tsx | 1 + src/components/PDFView/PdfPageItem.tsx | 34 ++++++++----- src/components/PDFView/index.tsx | 47 +++++++++++++----- src/pages/create/index.tsx | 58 +++++++++++------------ src/types/mark.ts | 1 + 7 files changed, 118 insertions(+), 77 deletions(-) diff --git a/src/components/FileList/style.module.scss b/src/components/FileList/style.module.scss index bbf9311..4103897 100644 --- a/src/components/FileList/style.module.scss +++ b/src/components/FileList/style.module.scss @@ -48,17 +48,17 @@ flex-grow: 1; font-size: 16px; font-weight: 500; + + &.active { + background: #4c82a3; + color: white; + } } .fileItem:hover { transition: ease 0.2s; background: #4c82a3; color: white; - - &.active { - background: #4c82a3; - color: white; - } } .fileName { diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx index eb5ceff..6e8aa64 100644 --- a/src/components/PDFView/PdfItem.tsx +++ b/src/components/PDFView/PdfItem.tsx @@ -1,6 +1,6 @@ import { PdfFile } from '../../types/drawing.ts' import { CurrentUserMark } from '../../types/mark.ts' -import PdfPageItem from './PdfPageItem.tsx'; +import PdfPageItem from './PdfPageItem.tsx' interface PdfItemProps { pdfFile: PdfFile @@ -13,23 +13,31 @@ interface PdfItemProps { /** * Responsible for displaying pages of a single Pdf File. */ -const PdfItem = ({ pdfFile, currentUserMarks, handleMarkClick, selectedMarkValue, selectedMark }: PdfItemProps) => { - const filterByPage = (marks: CurrentUserMark[], page: number): CurrentUserMark[] => { - return marks.filter((m) => m.mark.location.page === page); +const PdfItem = ({ + pdfFile, + currentUserMarks, + handleMarkClick, + selectedMarkValue, + selectedMark +}: PdfItemProps) => { + const filterByPage = ( + marks: CurrentUserMark[], + page: number + ): CurrentUserMark[] => { + return marks.filter((m) => m.mark.location.page === page) } - return ( - pdfFile.pages.map((page, i) => { - return ( - - ) - })) + return pdfFile.pages.map((page, i) => { + return ( + + ) + }) } -export default PdfItem \ No newline at end of file +export default PdfItem diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index fc49f79..1d4b522 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -126,6 +126,7 @@ const PdfMarking = (props: PdfMarkingProps) => { {currentUserMarks?.length > 0 && (
{ +const PdfPageItem = ({ + page, + currentUserMarks, + handleMarkClick, + selectedMarkValue, + selectedMark +}: PdfPageProps) => { + useEffect(() => { + if (selectedMark !== null && !!markRefs.current[selectedMark.id]) { + markRefs.current[selectedMark.id]?.scrollIntoView({ + behavior: 'smooth', + block: 'end' + }) + } + }, [selectedMark]) + const markRefs = useRef<(HTMLDivElement | null)[]>([]) return (
- - { - currentUserMarks.map((m, i) => ( + + {currentUserMarks.map((m, i) => ( +
(markRefs.current[m.id] = el)}> + /> +
))}
) } -export default PdfPageItem \ No newline at end of file +export default PdfPageItem diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx index 4024d1a..2936f59 100644 --- a/src/components/PDFView/index.tsx +++ b/src/components/PDFView/index.tsx @@ -1,7 +1,8 @@ -import { Box } from '@mui/material' +import { Box, Divider } from '@mui/material' import PdfItem from './PdfItem.tsx' import { CurrentUserMark } from '../../types/mark.ts' import { CurrentUserFile } from '../../types/file.ts' +import { useEffect, useRef } from 'react' interface PdfViewProps { files: CurrentUserFile[] @@ -9,6 +10,7 @@ interface PdfViewProps { handleMarkClick: (id: number) => void selectedMarkValue: string selectedMark: CurrentUserMark | null + currentFile: CurrentUserFile | null } /** @@ -19,29 +21,48 @@ const PdfView = ({ currentUserMarks, handleMarkClick, selectedMarkValue, - selectedMark + selectedMark, + currentFile }: PdfViewProps) => { + const pdfRefs = useRef<(HTMLDivElement | null)[]>([]) + useEffect(() => { + if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { + pdfRefs.current[currentFile.id]?.scrollIntoView({ + behavior: 'smooth', + block: 'end' + }) + } + }, [currentFile]) const filterByFile = ( currentUserMarks: CurrentUserMark[], - hash: string + fileName: string ): CurrentUserMark[] => { return currentUserMarks.filter( - (currentUserMark) => currentUserMark.mark.pdfFileHash === hash + (currentUserMark) => currentUserMark.mark.fileName === fileName ) } + const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean => + !(index !== files.length - 1) return ( - {files.map(({ pdfFile, hash }, i) => { + {files.map((currentUserFile, index, arr) => { + const { hash, pdfFile, filename, id } = currentUserFile if (!hash) return return ( - +
(pdfRefs.current[id] = el)} + key={index} + > + + {isNotLastPdfFile(index, arr) && File Separator} +
) })}
diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 830f9ef..8d6a278 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -338,32 +338,36 @@ export const CreatePage = () => { fileHashes[file.name] = hash } + console.log('file hashes: ', fileHashes) + return fileHashes } - 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 - } + 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 + } + }) }) }) - }) .map((mark, index) => { - return {...mark, id: index } - }); + return { ...mark, id: index } + }) } // Handle errors during zip file generation @@ -431,13 +435,9 @@ export const CreatePage = () => { if (!arraybuffer) return null - return new File( - [new Blob([arraybuffer])], - `${unixNow}.sigit.zip`, - { - type: 'application/zip' - } - ) + return new File([new Blob([arraybuffer])], `${unixNow}.sigit.zip`, { + type: 'application/zip' + }) } // Handle errors during file upload @@ -545,9 +545,7 @@ export const CreatePage = () => { : viewers.map((viewer) => viewer.pubkey) ).filter((receiver) => receiver !== usersPubkey) - return receivers.map((receiver) => - sendNotification(receiver, meta) - ) + return receivers.map((receiver) => sendNotification(receiver, meta)) } const handleCreate = async () => { diff --git a/src/types/mark.ts b/src/types/mark.ts index c0f9b88..efc1899 100644 --- a/src/types/mark.ts +++ b/src/types/mark.ts @@ -14,6 +14,7 @@ export interface Mark { pdfFileHash: string type: MarkType location: MarkLocation + fileName: string value?: string } From dfe67b99ad7d80b1ab7c3d41bd5f5281d1fc1f5e Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 13 Aug 2024 17:27:08 +0200 Subject: [PATCH 12/63] feat: add sticky layout with slots --- src/layouts/Files.module.scss | 24 ++++++++++++++++++++++++ src/layouts/Files.tsx | 29 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/layouts/Files.module.scss create mode 100644 src/layouts/Files.tsx diff --git a/src/layouts/Files.module.scss b/src/layouts/Files.module.scss new file mode 100644 index 0000000..bda18dc --- /dev/null +++ b/src/layouts/Files.module.scss @@ -0,0 +1,24 @@ +@import '../styles/colors.scss'; +@import '../styles/sizes.scss'; + +.container { + display: grid; + grid-template-columns: 0.75fr 1.5fr 0.75fr; + grid-gap: 30px; + flex-grow: 1; +} + +.sidesWrap { + position: relative; +} + +.sides { + position: sticky; + top: $header-height + $body-vertical-padding; +} + +.files { + display: flex; + flex-direction: column; + grid-gap: 15px; +} diff --git a/src/layouts/Files.tsx b/src/layouts/Files.tsx new file mode 100644 index 0000000..a494293 --- /dev/null +++ b/src/layouts/Files.tsx @@ -0,0 +1,29 @@ +import { PropsWithChildren, ReactNode } from 'react' + +import styles from './Files.module.scss' + +interface FilesProps { + left: ReactNode + right: ReactNode + content: ReactNode +} + +export const Files = ({ + left, + right, + content, + children +}: PropsWithChildren) => { + return ( +
+
+
{left}
+
+
{content}
+
+
{right}
+
+ {children} +
+ ) +} From 50c8f2b2d0806c820d5bbe39875f029e48bd38e7 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 11:20:48 +0200 Subject: [PATCH 13/63] refactor: rename Files to StickySideColumns and update css --- ...odule.scss => StickySideColumns.module.scss} | 5 +++++ .../{Files.tsx => StickySideColumns.tsx} | 17 +++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) rename src/layouts/{Files.module.scss => StickySideColumns.module.scss} (84%) rename src/layouts/{Files.tsx => StickySideColumns.tsx} (58%) diff --git a/src/layouts/Files.module.scss b/src/layouts/StickySideColumns.module.scss similarity index 84% rename from src/layouts/Files.module.scss rename to src/layouts/StickySideColumns.module.scss index bda18dc..63c4314 100644 --- a/src/layouts/Files.module.scss +++ b/src/layouts/StickySideColumns.module.scss @@ -22,3 +22,8 @@ flex-direction: column; grid-gap: 15px; } +.content { + max-width: 550px; + width: 100%; + margin: 0 auto; +} diff --git a/src/layouts/Files.tsx b/src/layouts/StickySideColumns.tsx similarity index 58% rename from src/layouts/Files.tsx rename to src/layouts/StickySideColumns.tsx index a494293..1ada87f 100644 --- a/src/layouts/Files.tsx +++ b/src/layouts/StickySideColumns.tsx @@ -1,29 +1,26 @@ import { PropsWithChildren, ReactNode } from 'react' -import styles from './Files.module.scss' +import styles from './StickySideColumns.module.scss' -interface FilesProps { - left: ReactNode - right: ReactNode - content: ReactNode +interface StickySideColumnsProps { + left?: ReactNode + right?: ReactNode } -export const Files = ({ +export const StickySideColumns = ({ left, right, - content, children -}: PropsWithChildren) => { +}: PropsWithChildren) => { return (
{left}
-
{content}
+
{children}
{right}
- {children}
) } From 64dbd7d479bb4baebc43d72f04dbedf7a18d02a7 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 14 Aug 2024 12:36:51 +0300 Subject: [PATCH 14/63] feat(pdf-marking): integrates layouts --- src/components/PDFView/PdfMarking.tsx | 50 +++++++++++++---------- src/layouts/StickySideColumns.module.scss | 2 +- src/pages/sign/style.module.scss | 4 +- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index 1d4b522..02a9627 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -13,6 +13,7 @@ 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' interface PdfMarkingProps { files: CurrentUserFile[] @@ -112,30 +113,35 @@ const PdfMarking = (props: PdfMarkingProps) => { return ( <> -
-
- {currentFile !== null && ( - + + {currentFile !== null && ( + + )} +
+ } + > +
+ {currentUserMarks?.length > 0 && ( +
+ +
)}
- {currentUserMarks?.length > 0 && ( -
- -
- )} -
+ {selectedMark !== null && ( Date: Wed, 14 Aug 2024 14:25:33 +0200 Subject: [PATCH 15/63] feat: add simple spinner wrapper --- src/components/Spinner/index.tsx | 6 ++++++ src/components/Spinner/style.module.scss | 12 ++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/components/Spinner/index.tsx create mode 100644 src/components/Spinner/style.module.scss diff --git a/src/components/Spinner/index.tsx b/src/components/Spinner/index.tsx new file mode 100644 index 0000000..cbc6b43 --- /dev/null +++ b/src/components/Spinner/index.tsx @@ -0,0 +1,6 @@ +import { PropsWithChildren } from 'react' +import styles from './style.module.scss' + +export const Spinner = ({ children }: PropsWithChildren) => ( +
{children}
+) diff --git a/src/components/Spinner/style.module.scss b/src/components/Spinner/style.module.scss new file mode 100644 index 0000000..08d8032 --- /dev/null +++ b/src/components/Spinner/style.module.scss @@ -0,0 +1,12 @@ +.spin { + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} From 4b04bdf39e5cb66c9c4ae45623d4f86432628a32 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 14:27:49 +0200 Subject: [PATCH 16/63] refactor: useSigitMeta, DisplaySigners and UserDetails section --- src/components/DisplaySigit/index.tsx | 5 +- src/components/DisplaySigner/index.tsx | 81 +++--- src/components/FilesUsers.tsx/index.tsx | 246 ------------------ src/components/UsersDetails.tsx/index.tsx | 224 ++++++++++++++++ .../style.module.scss | 5 + src/hooks/useSigitMeta.tsx | 82 ++++-- src/pages/verify/index.tsx | 67 ++++- src/utils/meta.ts | 4 +- 8 files changed, 384 insertions(+), 330 deletions(-) delete mode 100644 src/components/FilesUsers.tsx/index.tsx create mode 100644 src/components/UsersDetails.tsx/index.tsx rename src/components/{FilesUsers.tsx => UsersDetails.tsx}/style.module.scss (93%) diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index dfbcbba..0d7407f 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -20,6 +20,7 @@ import styles from './style.module.scss' import { TooltipChild } from '../TooltipChild' import { getExtensionIconLabel } from '../getExtensionIconLabel' import { useSigitProfiles } from '../../hooks/useSigitProfiles' +import { useSigitMeta } from '../../hooks/useSigitMeta' type SigitProps = { meta: Meta @@ -36,6 +37,8 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { fileExtensions } = parsedMeta + const { signersStatus } = useSigitMeta(meta) + const profiles = useSigitProfiles([ ...(submittedBy ? [submittedBy] : []), ...signers @@ -94,7 +97,7 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { > diff --git a/src/components/DisplaySigner/index.tsx b/src/components/DisplaySigner/index.tsx index dc4b9ce..4f3824f 100644 --- a/src/components/DisplaySigner/index.tsx +++ b/src/components/DisplaySigner/index.tsx @@ -1,58 +1,50 @@ import { Badge } from '@mui/material' -import { Event, verifyEvent } from 'nostr-tools' -import { useState, useEffect } from 'react' -import { Meta, ProfileMetadata } from '../../types' -import { hexToNpub, parseJson } from '../../utils' +import { ProfileMetadata } from '../../types' import styles from './style.module.scss' import { UserAvatar } from '../UserAvatar' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faCheck, faExclamation } from '@fortawesome/free-solid-svg-icons' - -enum SignStatus { - Signed = 'Signed', - Pending = 'Pending', - Invalid = 'Invalid Sign' -} +import { + faCheck, + faExclamation, + faEye, + faHourglass, + faQuestion +} from '@fortawesome/free-solid-svg-icons' +import { SignStatus } from '../../utils' +import { Spinner } from '../Spinner' type DisplaySignerProps = { - meta: Meta profile: ProfileMetadata pubkey: string + status: SignStatus } export const DisplaySigner = ({ - meta, + status, profile, pubkey }: DisplaySignerProps) => { - const [signStatus, setSignedStatus] = useState() + const getStatusIcon = (status: SignStatus) => { + switch (status) { + case SignStatus.Signed: + return + case SignStatus.Awaiting: + return ( + + + + ) + case SignStatus.Pending: + return + case SignStatus.Invalid: + return + case SignStatus.Viewer: + return - useEffect(() => { - if (!meta) return - - const updateSignStatus = async () => { - const npub = hexToNpub(pubkey) - if (npub in meta.docSignatures) { - parseJson(meta.docSignatures[npub]) - .then((event) => { - const isValidSignature = verifyEvent(event) - if (isValidSignature) { - setSignedStatus(SignStatus.Signed) - } else { - setSignedStatus(SignStatus.Invalid) - } - }) - .catch((err) => { - console.log(`err in parsing the docSignatures for ${npub}:>> `, err) - setSignedStatus(SignStatus.Invalid) - }) - } else { - setSignedStatus(SignStatus.Pending) - } + default: + return } - - updateSignStatus() - }, [meta, pubkey]) + } return ( - {signStatus === SignStatus.Signed && ( - - )} - {signStatus === SignStatus.Invalid && ( - - )} -
- ) +
{getStatusIcon(status)}
} > diff --git a/src/components/FilesUsers.tsx/index.tsx b/src/components/FilesUsers.tsx/index.tsx deleted file mode 100644 index 59e3a09..0000000 --- a/src/components/FilesUsers.tsx/index.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import { CheckCircle, Cancel } from '@mui/icons-material' -import { Divider, Tooltip } from '@mui/material' -import { verifyEvent } from 'nostr-tools' -import { useSigitProfiles } from '../../hooks/useSigitProfiles' -import { Meta, SignedEventContent } from '../../types' -import { - extractFileExtensions, - formatTimestamp, - fromUnixTimestamp, - hexToNpub, - npubToHex, - shorten -} from '../../utils' -import { UserAvatar } from '../UserAvatar' -import { useSigitMeta } from '../../hooks/useSigitMeta' -import { UserAvatarGroup } from '../UserAvatarGroup' - -import styles from './style.module.scss' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { - faCalendar, - faCalendarCheck, - faCalendarPlus, - faEye, - faFile, - faFileCircleExclamation -} from '@fortawesome/free-solid-svg-icons' -import { getExtensionIconLabel } from '../getExtensionIconLabel' -import { useSelector } from 'react-redux' -import { State } from '../../store/rootReducer' - -interface FileUsersProps { - meta: Meta -} - -export const FileUsers = ({ meta }: FileUsersProps) => { - const { usersPubkey } = useSelector((state: State) => state.auth) - const { - submittedBy, - signers, - viewers, - fileHashes, - sig, - docSignatures, - parsedSignatureEvents, - createdAt, - signedStatus, - completedAt - } = useSigitMeta(meta) - const profiles = useSigitProfiles([ - ...(submittedBy ? [submittedBy] : []), - ...signers, - ...viewers - ]) - const userCanSign = - typeof usersPubkey !== 'undefined' && - signers.includes(hexToNpub(usersPubkey)) - - const ext = extractFileExtensions(Object.keys(fileHashes)) - - const getPrevSignersSig = (npub: string) => { - // if user is first signer then use creator's signature - if (signers[0] === npub) { - return sig - } - - // find the index of signer - const currentSignerIndex = signers.findIndex((signer) => signer === npub) - // return null if could not found user in signer's list - if (currentSignerIndex === -1) return null - // find prev signer - const prevSigner = signers[currentSignerIndex - 1] - - // get the signature of prev signer - try { - const prevSignersEvent = parsedSignatureEvents[prevSigner] - return prevSignersEvent.sig - } catch (error) { - return null - } - } - - const displayUser = (pubkey: string, verifySignature = false) => { - const profile = profiles[pubkey] - - let isValidSignature = false - - if (verifySignature) { - const npub = hexToNpub(pubkey) - const signedEventString = docSignatures[npub] - if (signedEventString) { - try { - const signedEvent = JSON.parse(signedEventString) - const isVerifiedEvent = verifyEvent(signedEvent) - - if (isVerifiedEvent) { - // get the actual signature of prev signer - const prevSignersSig = getPrevSignersSig(npub) - - // get the signature of prev signer from the content of current signers signedEvent - - try { - const obj: SignedEventContent = JSON.parse(signedEvent.content) - if ( - obj.prevSig && - prevSignersSig && - obj.prevSig === prevSignersSig - ) { - isValidSignature = true - } - } catch (error) { - isValidSignature = false - } - } - } catch (error) { - console.error( - `An error occurred in parsing and verifying the signature event for ${pubkey}`, - error - ) - } - } - } - - return ( - <> - - - {verifySignature && ( - <> - {isValidSignature && ( - - - - )} - - {!isValidSignature && ( - - - - )} - - )} - - ) - } - - return submittedBy ? ( -
-
-

Signers

- {displayUser(submittedBy)} - {submittedBy && signers.length ? ( - - ) : null} - - {signers.length > 0 && - signers.map((signer) => ( - {displayUser(npubToHex(signer)!, true)} - ))} - {viewers.length > 0 && - viewers.map((viewer) => ( - {displayUser(npubToHex(viewer)!)} - ))} - -
-
-

Details

- - - - {' '} - {createdAt ? formatTimestamp(createdAt) : <>—} - - - - - - {' '} - {completedAt ? formatTimestamp(completedAt) : <>—} - - - - {/* User signed date */} - {userCanSign ? ( - - - {' '} - {hexToNpub(usersPubkey) in parsedSignatureEvents ? ( - parsedSignatureEvents[hexToNpub(usersPubkey)].created_at ? ( - formatTimestamp( - fromUnixTimestamp( - parsedSignatureEvents[hexToNpub(usersPubkey)].created_at - ) - ) - ) : ( - <>— - ) - ) : ( - <>— - )} - - - ) : null} - - {signedStatus} - - {ext.length > 0 ? ( - - {ext.length > 1 ? ( - <> - Multiple File Types - - ) : ( - getExtensionIconLabel(ext[0]) - )} - - ) : ( - <> - — - - )} -
-
- ) : undefined -} diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx new file mode 100644 index 0000000..8b9217b --- /dev/null +++ b/src/components/UsersDetails.tsx/index.tsx @@ -0,0 +1,224 @@ +import { Divider, Tooltip } from '@mui/material' +import { useSigitProfiles } from '../../hooks/useSigitProfiles' +import { + extractFileExtensions, + formatTimestamp, + fromUnixTimestamp, + hexToNpub, + npubToHex, + shorten, + SignStatus +} from '../../utils' +import { UserAvatar } from '../UserAvatar' +import { FlatMeta } from '../../hooks/useSigitMeta' +import { UserAvatarGroup } from '../UserAvatarGroup' + +import styles from './style.module.scss' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { + faCalendar, + faCalendarCheck, + faCalendarPlus, + faEye, + faFile, + faFileCircleExclamation +} from '@fortawesome/free-solid-svg-icons' +import { getExtensionIconLabel } from '../getExtensionIconLabel' +import { useSelector } from 'react-redux' +import { State } from '../../store/rootReducer' +import { TooltipChild } from '../TooltipChild' +import { DisplaySigner } from '../DisplaySigner' + +type UsersDetailsProps = Pick< + FlatMeta, + | 'submittedBy' + | 'signers' + | 'viewers' + | 'fileHashes' + | 'parsedSignatureEvents' + | 'createdAt' + | 'signedStatus' + | 'completedAt' + | 'signersStatus' +> + +export const UsersDetails = ({ + submittedBy, + signers, + viewers, + fileHashes, + parsedSignatureEvents, + createdAt, + signedStatus, + completedAt, + signersStatus +}: UsersDetailsProps) => { + const { usersPubkey } = useSelector((state: State) => state.auth) + const profiles = useSigitProfiles([ + ...(submittedBy ? [submittedBy] : []), + ...signers, + ...viewers + ]) + const userCanSign = + typeof usersPubkey !== 'undefined' && + signers.includes(hexToNpub(usersPubkey)) + + const ext = extractFileExtensions(Object.keys(fileHashes)) + + return submittedBy ? ( +
+
+

Signers

+
+ {submittedBy && + (function () { + const profile = profiles[submittedBy] + return ( + + + + + + ) + })()} + + {submittedBy && signers.length ? ( + + ) : null} + + + {signers.map((signer) => { + const pubkey = npubToHex(signer)! + const profile = profiles[pubkey] + + return ( + + + + + + ) + })} + {viewers.map((signer) => { + const pubkey = npubToHex(signer)! + const profile = profiles[pubkey] + + return ( + + + + + + ) + })} + +
+
+
+

Details

+ + + + {' '} + {createdAt ? formatTimestamp(createdAt) : <>—} + + + + + + {' '} + {completedAt ? formatTimestamp(completedAt) : <>—} + + + + {/* User signed date */} + {userCanSign ? ( + + + {' '} + {hexToNpub(usersPubkey) in parsedSignatureEvents ? ( + parsedSignatureEvents[hexToNpub(usersPubkey)].created_at ? ( + formatTimestamp( + fromUnixTimestamp( + parsedSignatureEvents[hexToNpub(usersPubkey)].created_at + ) + ) + ) : ( + <>— + ) + ) : ( + <>— + )} + + + ) : null} + + {signedStatus} + + {ext.length > 0 ? ( + + {ext.length > 1 ? ( + <> + Multiple File Types + + ) : ( + getExtensionIconLabel(ext[0]) + )} + + ) : ( + <> + — + + )} +
+
+ ) : undefined +} diff --git a/src/components/FilesUsers.tsx/style.module.scss b/src/components/UsersDetails.tsx/style.module.scss similarity index 93% rename from src/components/FilesUsers.tsx/style.module.scss rename to src/components/UsersDetails.tsx/style.module.scss index b6e0313..9d906c1 100644 --- a/src/components/FilesUsers.tsx/style.module.scss +++ b/src/components/UsersDetails.tsx/style.module.scss @@ -17,6 +17,11 @@ grid-gap: 10px; } +.users { + display: flex; + grid-gap: 10px; +} + .detailsItem { transition: ease 0.2s; color: rgba(0, 0, 0, 0.5); diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index 78516d5..ca8ea6d 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { CreateSignatureEventContent, Meta } from '../types' +import { CreateSignatureEventContent, Meta, SignedEventContent } from '../types' import { Mark } from '../types/mark' import { fromUnixTimestamp, @@ -118,7 +118,6 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { if (meta.keys) { const { sender, keys } = meta.keys - // Retrieve the user's public key from the state const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const usersNpub = hexToNpub(usersPubkey) @@ -141,8 +140,28 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { } } - // Temp. map to hold events + // Temp. map to hold events and signers const parsedSignatureEventsMap = new Map<`npub1${string}`, Event>() + const signerStatusMap = new Map<`npub1${string}`, SignStatus>() + + const getPrevSignerSig = (npub: `npub1${string}`) => { + if (signers[0] === npub) { + return sig + } + + // find the index of signer + const currentSignerIndex = signers.findIndex( + (signer) => signer === npub + ) + // return if could not found user in signer's list + if (currentSignerIndex === -1) return + // find prev signer + const prevSigner = signers[currentSignerIndex - 1] + + // get the signature of prev signer + return parsedSignatureEventsMap.get(prevSigner)?.sig + } + for (const npub in meta.docSignatures) { try { // Parse each signature event @@ -150,34 +169,49 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { meta.docSignatures[npub as `npub1${string}`] ) - const isValidSignature = verifyEvent(event) - // Save events to a map, to save all at once outside loop // We need the object to find completedAt // Avoided using parsedSignatureEvents due to useEffect deps parsedSignatureEventsMap.set(npub as `npub1${string}`, event) - - setSignersStatus((prev) => { - return { - ...prev, - [npub]: isValidSignature - ? SignStatus.Signed - : SignStatus.Invalid - } - }) } catch (error) { - setSignersStatus((prev) => { - return { - ...prev, - [npub]: SignStatus.Invalid - } - }) + signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid) } } - setParsedSignatureEvents( - Object.fromEntries(parsedSignatureEventsMap.entries()) - ) + parsedSignatureEventsMap.forEach((event, npub) => { + const isValidSignature = verifyEvent(event) + if (isValidSignature) { + // get the signature of prev signer from the content of current signers signedEvent + const prevSignersSig = getPrevSignerSig(npub) + + try { + const obj: SignedEventContent = JSON.parse(event.content) + if ( + obj.prevSig && + prevSignersSig && + obj.prevSig === prevSignersSig + ) { + signerStatusMap.set(npub as `npub1${string}`, SignStatus.Signed) + } + } catch (error) { + signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid) + } + } + }) + + const nextSigner = signers.find((s) => !parsedSignatureEventsMap.has(s)) + if (nextSigner) { + signerStatusMap.set(nextSigner, SignStatus.Awaiting) + } + + signers + .filter((s) => !(s in meta.docSignatures)) + .forEach((s) => + signerStatusMap.set(s as `npub1${string}`, SignStatus.Pending) + ) + + setSignersStatus(Object.fromEntries(signerStatusMap)) + setParsedSignatureEvents(Object.fromEntries(parsedSignatureEventsMap)) const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] const isCompletelySigned = signers.every((signer) => @@ -187,7 +221,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial ) - // Check if all signers signed and are valid + // Check if all signers signed if (isCompletelySigned) { setCompletedAt( fromUnixTimestamp( diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index d6a297d..bd35f14 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -15,7 +15,8 @@ import { unixNow, parseJson, readContentOfZipEntry, - signEventForMetaFile + signEventForMetaFile, + shorten } from '../../utils' import styles from './style.module.scss' import { Cancel, CheckCircle } from '@mui/icons-material' @@ -34,8 +35,11 @@ import { getLastSignersSig } from '../../utils/sign.ts' import { saveAs } from 'file-saver' import { Container } from '../../components/Container' import { useSigitMeta } from '../../hooks/useSigitMeta.tsx' -import { Files } from '../../layouts/Files.tsx' -import { FileUsers } from '../../components/FilesUsers.tsx/index.tsx' +import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' +import { UsersDetails } from '../../components/UsersDetails.tsx/index.tsx' +import { UserAvatar } from '../../components/UserAvatar/index.tsx' +import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx' +import { TooltipChild } from '../../components/TooltipChild.tsx' export const VerifyPage = () => { const theme = useTheme() @@ -50,8 +54,25 @@ export const VerifyPage = () => { */ const { uploadedZip, meta } = location.state || {} - const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } = - useSigitMeta(meta) + const { + submittedBy, + zipUrl, + encryptionKey, + signers, + viewers, + fileHashes, + parsedSignatureEvents, + createdAt, + signedStatus, + completedAt, + signersStatus + } = useSigitMeta(meta) + + const profiles = useSigitProfiles([ + ...(submittedBy ? [submittedBy] : []), + ...signers, + ...viewers + ]) const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -339,7 +360,24 @@ export const VerifyPage = () => { const exportSignatureEvent = JSON.parse(exportSignatureString) as Event if (verifyEvent(exportSignatureEvent)) { - // return displayUser(exportSignatureEvent.pubkey) + const exportedBy = exportSignatureEvent.pubkey + const profile = profiles[exportedBy] + return ( + + + + + + ) } else { toast.error(`Invalid export signature!`) return ( @@ -386,7 +424,7 @@ export const VerifyPage = () => { )} {meta && ( - @@ -432,8 +470,19 @@ export const VerifyPage = () => { } - right={} - content={
} + right={ + + } /> )} diff --git a/src/utils/meta.ts b/src/utils/meta.ts index dd29b60..4915f19 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -5,8 +5,10 @@ import { toast } from 'react-toastify' export enum SignStatus { Signed = 'Signed', + Awaiting = 'Awaiting', Pending = 'Pending', - Invalid = 'Invalid Sign' + Invalid = 'Invalid', + Viewer = 'Viewer' } export enum SigitStatus { From 3743a30ef62084c6c3a8cfdfcb63d1f08f0162ed Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 14:31:26 +0200 Subject: [PATCH 17/63] fix: use correct key for signer status, update signer badge icons --- src/components/DisplaySigner/index.tsx | 3 ++- src/components/UsersDetails.tsx/index.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/DisplaySigner/index.tsx b/src/components/DisplaySigner/index.tsx index 4f3824f..63aa154 100644 --- a/src/components/DisplaySigner/index.tsx +++ b/src/components/DisplaySigner/index.tsx @@ -5,6 +5,7 @@ import { UserAvatar } from '../UserAvatar' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCheck, + faEllipsis, faExclamation, faEye, faHourglass, @@ -35,7 +36,7 @@ export const DisplaySigner = ({ ) case SignStatus.Pending: - return + return case SignStatus.Invalid: return case SignStatus.Viewer: diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx index 8b9217b..ea58f68 100644 --- a/src/components/UsersDetails.tsx/index.tsx +++ b/src/components/UsersDetails.tsx/index.tsx @@ -112,7 +112,7 @@ export const UsersDetails = ({ > From d8adb2c74471bf55351b3649c699f8b6d4360a47 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 14:34:51 +0200 Subject: [PATCH 18/63] fix: next signer and spinner anim duration --- src/components/Spinner/style.module.scss | 2 +- src/hooks/useSigitMeta.tsx | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/Spinner/style.module.scss b/src/components/Spinner/style.module.scss index 08d8032..60158f4 100644 --- a/src/components/Spinner/style.module.scss +++ b/src/components/Spinner/style.module.scss @@ -1,5 +1,5 @@ .spin { - animation: spin 1s linear infinite; + animation: spin 5s linear infinite; } @keyframes spin { diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index ca8ea6d..a393824 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -199,17 +199,16 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { } }) + signers + .filter((s) => !parsedSignatureEventsMap.has(s)) + .forEach((s) => signerStatusMap.set(s, SignStatus.Pending)) + + // Get the first signer that hasn't signed const nextSigner = signers.find((s) => !parsedSignatureEventsMap.has(s)) if (nextSigner) { signerStatusMap.set(nextSigner, SignStatus.Awaiting) } - signers - .filter((s) => !(s in meta.docSignatures)) - .forEach((s) => - signerStatusMap.set(s as `npub1${string}`, SignStatus.Pending) - ) - setSignersStatus(Object.fromEntries(signerStatusMap)) setParsedSignatureEvents(Object.fromEntries(parsedSignatureEventsMap)) From 1c3d3ca88ff951a83bb1689384ea807733c0e14c Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 14:44:11 +0200 Subject: [PATCH 19/63] refactor: pass meta to UserDetails instead of individual props --- src/components/UsersDetails.tsx/index.tsx | 41 ++++++++++------------- src/pages/verify/index.tsx | 29 ++-------------- 2 files changed, 20 insertions(+), 50 deletions(-) diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx index ea58f68..fc7d43d 100644 --- a/src/components/UsersDetails.tsx/index.tsx +++ b/src/components/UsersDetails.tsx/index.tsx @@ -10,7 +10,7 @@ import { SignStatus } from '../../utils' import { UserAvatar } from '../UserAvatar' -import { FlatMeta } from '../../hooks/useSigitMeta' +import { useSigitMeta } from '../../hooks/useSigitMeta' import { UserAvatarGroup } from '../UserAvatarGroup' import styles from './style.module.scss' @@ -28,31 +28,24 @@ import { useSelector } from 'react-redux' import { State } from '../../store/rootReducer' import { TooltipChild } from '../TooltipChild' import { DisplaySigner } from '../DisplaySigner' +import { Meta } from '../../types' -type UsersDetailsProps = Pick< - FlatMeta, - | 'submittedBy' - | 'signers' - | 'viewers' - | 'fileHashes' - | 'parsedSignatureEvents' - | 'createdAt' - | 'signedStatus' - | 'completedAt' - | 'signersStatus' -> +interface UsersDetailsProps { + meta: Meta +} -export const UsersDetails = ({ - submittedBy, - signers, - viewers, - fileHashes, - parsedSignatureEvents, - createdAt, - signedStatus, - completedAt, - signersStatus -}: UsersDetailsProps) => { +export const UsersDetails = ({ meta }: UsersDetailsProps) => { + const { + submittedBy, + signers, + viewers, + fileHashes, + signersStatus, + createdAt, + completedAt, + parsedSignatureEvents, + signedStatus + } = useSigitMeta(meta) const { usersPubkey } = useSelector((state: State) => state.auth) const profiles = useSigitProfiles([ ...(submittedBy ? [submittedBy] : []), diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index bd35f14..d991666 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -54,19 +54,8 @@ export const VerifyPage = () => { */ const { uploadedZip, meta } = location.state || {} - const { - submittedBy, - zipUrl, - encryptionKey, - signers, - viewers, - fileHashes, - parsedSignatureEvents, - createdAt, - signedStatus, - completedAt, - signersStatus - } = useSigitMeta(meta) + const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } = + useSigitMeta(meta) const profiles = useSigitProfiles([ ...(submittedBy ? [submittedBy] : []), @@ -470,19 +459,7 @@ export const VerifyPage = () => { } - right={ - - } + right={} /> )} From d41d577c29af2135e4352186af1b4b434d22cc95 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 14 Aug 2024 16:08:42 +0300 Subject: [PATCH 20/63] fix: styling --- .../DrawPDFFields/style.module.scss | 1 + src/components/FileList/index.tsx | 29 ++++++++++--------- src/components/FileList/style.module.scss | 28 ++++++++++++++++++ src/components/PDFView/PdfPageItem.tsx | 4 +-- src/components/PDFView/index.tsx | 6 ++-- src/components/PDFView/style.module.scss | 2 ++ 6 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index f1993eb..1b13c31 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -50,6 +50,7 @@ .pdfImageWrapper { position: relative; user-select: none; + margin-bottom: 10px; &.drawing { cursor: crosshair; diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx index ba69332..f1fdcd8 100644 --- a/src/components/FileList/index.tsx +++ b/src/components/FileList/index.tsx @@ -17,20 +17,21 @@ const FileList = ({ }: FileListProps) => { const isActive = (file: CurrentUserFile) => file.id === currentFile.id return ( -
-
-
    - {files.map((file: CurrentUserFile) => ( -
  • setCurrentFile(file)} - > - {file.id} - {file.filename} -
  • - ))} -
+
+
+
    + {files.map((file: CurrentUserFile) => ( +
  • setCurrentFile(file)} + > + {file.id} + {file.filename} +
  • + ))} +
+
diff --git a/src/components/FileList/style.module.scss b/src/components/FileList/style.module.scss index 4103897..b754bdb 100644 --- a/src/components/FileList/style.module.scss +++ b/src/components/FileList/style.module.scss @@ -7,6 +7,29 @@ grid-gap: 0px; } +.filesPageContainer { + width: 100%; + display: grid; + grid-template-columns: 0.75fr 1.5fr 0.75fr; + grid-gap: 30px; + flex-grow: 1; +} + +ul { + list-style-type: none; /* Removes bullet points */ + margin: 0; /* Removes default margin */ + padding: 0; /* Removes default padding */ +} + + +.wrap { + position: sticky; + top: 15px; + display: flex; + flex-direction: column; + grid-gap: 15px; +} + .files { display: flex; flex-direction: column; @@ -33,6 +56,10 @@ cursor: grab; } +.wrap { + margin-top: 5px; +} + .fileItem { transition: ease 0.2s; display: flex; @@ -49,6 +76,7 @@ font-size: 16px; font-weight: 500; + &.active { background: #4c82a3; color: white; diff --git a/src/components/PDFView/PdfPageItem.tsx b/src/components/PDFView/PdfPageItem.tsx index 01ab512..a670f2f 100644 --- a/src/components/PDFView/PdfPageItem.tsx +++ b/src/components/PDFView/PdfPageItem.tsx @@ -34,9 +34,7 @@ const PdfPageItem = ({
diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx index 2936f59..c032a3c 100644 --- a/src/components/PDFView/index.tsx +++ b/src/components/PDFView/index.tsx @@ -42,9 +42,9 @@ const PdfView = ({ ) } const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean => - !(index !== files.length - 1) + index !== files.length - 1 return ( - + <> {files.map((currentUserFile, index, arr) => { const { hash, pdfFile, filename, id } = currentUserFile if (!hash) return @@ -65,7 +65,7 @@ const PdfView = ({
) })} - + ) } diff --git a/src/components/PDFView/style.module.scss b/src/components/PDFView/style.module.scss index 3ebbc85..5029747 100644 --- a/src/components/PDFView/style.module.scss +++ b/src/components/PDFView/style.module.scss @@ -19,6 +19,7 @@ display: flex; width: 100%; flex-direction: column; + } .pdfView { @@ -26,4 +27,5 @@ flex-direction: column; width: 100%; height: 100%; + gap: 10px; } From 2f29ea9f35ad1c3285a9c01e7e51dfc37942c02f Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 14 Aug 2024 16:34:20 +0300 Subject: [PATCH 21/63] fix: styling --- src/components/DrawPDFFields/style.module.scss | 7 +++++++ src/components/FileList/style.module.scss | 6 ------ src/components/MarkFormField/style.module.scss | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 1b13c31..8773d0d 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -51,6 +51,13 @@ position: relative; user-select: none; margin-bottom: 10px; + + > img { + display: block; + max-width: 100%; + max-height: 100%; + object-fit: contain; /* Ensure the image fits within the container */ + } &.drawing { cursor: crosshair; diff --git a/src/components/FileList/style.module.scss b/src/components/FileList/style.module.scss index b754bdb..40b83a3 100644 --- a/src/components/FileList/style.module.scss +++ b/src/components/FileList/style.module.scss @@ -23,8 +23,6 @@ ul { .wrap { - position: sticky; - top: 15px; display: flex; flex-direction: column; grid-gap: 15px; @@ -56,10 +54,6 @@ ul { cursor: grab; } -.wrap { - margin-top: 5px; -} - .fileItem { transition: ease 0.2s; display: flex; diff --git a/src/components/MarkFormField/style.module.scss b/src/components/MarkFormField/style.module.scss index f3fa5d5..b5c6bb9 100644 --- a/src/components/MarkFormField/style.module.scss +++ b/src/components/MarkFormField/style.module.scss @@ -7,6 +7,7 @@ right: 0; left: 0; align-items: center; + z-index: 1000; } .actions { From 2becab9f79e1cb3aaf91178d67c70f9e98c4f98b Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 15 Aug 2024 12:23:35 +0300 Subject: [PATCH 22/63] feat(pdf-marking): integrates UserDetails --- src/components/PDFView/PdfMarking.tsx | 11 ++++++++--- src/pages/sign/index.tsx | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index 943b032..8d3a369 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -14,6 +14,8 @@ import styles from './style.module.scss' import { CurrentUserFile } from '../../types/file.ts' import FileList from '../FileList' import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' +import { UsersDetails } from '../UsersDetails.tsx' +import { Meta } from '../../types' interface PdfMarkingProps { files: CurrentUserFile[] @@ -22,6 +24,7 @@ interface PdfMarkingProps { setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void setUpdatedMarks: (markToUpdate: Mark) => void handleDownload: () => void + meta: Meta | null } /** @@ -37,7 +40,8 @@ const PdfMarking = (props: PdfMarkingProps) => { setIsReadyToSign, setCurrentUserMarks, setUpdatedMarks, - handleDownload + handleDownload, + meta } = props const [selectedMark, setSelectedMark] = useState(null) const [selectedMarkValue, setSelectedMarkValue] = useState('') @@ -81,8 +85,8 @@ const PdfMarking = (props: PdfMarkingProps) => { } const handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); - if (!selectedMarkValue || !selectedMark) return; + event.preventDefault() + if (!selectedMarkValue || !selectedMark) return const updatedMark: CurrentUserMark = getUpdatedMark( selectedMark, @@ -126,6 +130,7 @@ const PdfMarking = (props: PdfMarkingProps) => { )}
} + right={meta !== null && } >
{currentUserMarks?.length > 0 && ( diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 72ee3f4..9f2ab7c 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -958,6 +958,7 @@ export const SignPage = () => { setCurrentUserMarks={setCurrentUserMarks} setUpdatedMarks={setUpdatedMarks} handleDownload={handleDownload} + meta={meta} /> ) } From b22f577cc2de21bb1f8370c84a1d263f09fd7eb6 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 15 Aug 2024 12:25:37 +0300 Subject: [PATCH 23/63] fix: marking --- src/components/PDFView/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx index 3ac05d0..d67d372 100644 --- a/src/components/PDFView/index.tsx +++ b/src/components/PDFView/index.tsx @@ -35,10 +35,10 @@ const PdfView = ({ }, [currentFile]) const filterByFile = ( currentUserMarks: CurrentUserMark[], - fileName: string + hash: string ): CurrentUserMark[] => { return currentUserMarks.filter( - (currentUserMark) => currentUserMark.mark.fileName === fileName + (currentUserMark) => currentUserMark.mark.pdfFileHash === hash ) } const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean => @@ -46,7 +46,7 @@ const PdfView = ({ return ( <> {files.map((currentUserFile, index, arr) => { - const { hash, pdfFile, filename, id } = currentUserFile + const { hash, pdfFile, id } = currentUserFile if (!hash) return return (
Date: Thu, 15 Aug 2024 15:35:37 +0300 Subject: [PATCH 24/63] feat(pdf-marking): adds file validity check --- src/components/FileList/index.tsx | 12 ++++++++++-- src/components/FileList/style.module.scss | 23 +++++++++++++++++++++++ src/pages/sign/index.tsx | 2 +- src/types/file.ts | 1 + src/utils/utils.ts | 7 +++++-- 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx index f1fdcd8..7cd30eb 100644 --- a/src/components/FileList/index.tsx +++ b/src/components/FileList/index.tsx @@ -1,6 +1,8 @@ import { CurrentUserFile } from '../../types/file.ts' import styles from './style.module.scss' import { Button } from '@mui/material' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheck } from '@fortawesome/free-solid-svg-icons' interface FileListProps { files: CurrentUserFile[] @@ -26,8 +28,14 @@ const FileList = ({ className={`${styles.fileItem} ${isActive(file) && styles.active}`} onClick={() => setCurrentFile(file)} > - {file.id} - {file.filename} +
{file.id}
+
+
{file.filename}
+
+ +
+ {file.isHashValid && } +
))} diff --git a/src/components/FileList/style.module.scss b/src/components/FileList/style.module.scss index 40b83a3..6f7b64a 100644 --- a/src/components/FileList/style.module.scss +++ b/src/components/FileList/style.module.scss @@ -21,6 +21,12 @@ ul { padding: 0; /* Removes default padding */ } +li { + list-style-type: none; /* Removes the bullets */ + margin: 0; /* Removes any default margin */ + padding: 0; /* Removes any default padding */ +} + .wrap { display: flex; @@ -83,6 +89,12 @@ ul { color: white; } +.fileInfo { + flex-grow: 1; + font-size: 16px; + font-weight: 500; +} + .fileName { display: -webkit-box; -webkit-box-orient: vertical; @@ -97,4 +109,15 @@ ul { flex-direction: column; justify-content: center; align-items: center; + width: 10px; +} + +.fileVisual { + display: flex; + flex-shrink: 0; + flex-direction: column; + justify-content: center; + align-items: center; + height: 25px; + width: 25px; } \ No newline at end of file diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 9f2ab7c..a762292 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -952,7 +952,7 @@ export const SignPage = () => { return ( { * including its name, hash, and content * @param files * @param fileHashes + * @param creatorFileHashes */ export const getCurrentUserFiles = ( files: { [filename: string]: PdfFile }, - fileHashes: { [key: string]: string | null } + fileHashes: { [key: string]: string | null }, + creatorFileHashes: { [key: string]: string } ): CurrentUserFile[] => { return Object.entries(files).map(([filename, pdfFile], index) => { return { pdfFile, filename, id: index + 1, - ...(!!fileHashes[filename] && { hash: fileHashes[filename]! }) + ...(!!fileHashes[filename] && { hash: fileHashes[filename]! }), + isHashValid: creatorFileHashes[filename] === fileHashes[filename] } }) } From a775d7b265594d575f106898585b8f1dcebbce6f Mon Sep 17 00:00:00 2001 From: daniyal Date: Thu, 15 Aug 2024 22:13:39 +0500 Subject: [PATCH 25/63] feat: implemented relay controller and use that for fetching and publishing events --- src/controllers/MetadataController.ts | 79 ++++--- src/controllers/NostrController.ts | 157 +------------- src/controllers/RelayController.ts | 293 ++++++++++++++++++++++++++ src/controllers/index.ts | 1 + src/layouts/Main.tsx | 22 +- src/utils/index.ts | 12 +- src/utils/nostr.ts | 74 ++++--- src/utils/relays.ts | 63 +++++- src/utils/url.ts | 47 +++++ 9 files changed, 505 insertions(+), 243 deletions(-) create mode 100644 src/controllers/RelayController.ts create mode 100644 src/utils/url.ts diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index 8f4d190..b9557bb 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -1,28 +1,29 @@ +import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk' import { - Filter, - SimplePool, - VerifiedEvent, - kinds, - validateEvent, - verifyEvent, Event, EventTemplate, - nip19 + Filter, + VerifiedEvent, + kinds, + nip19, + validateEvent, + verifyEvent } from 'nostr-tools' -import { NostrJoiningBlock, ProfileMetadata, RelaySet } from '../types' -import { NostrController } from '.' import { toast } from 'react-toastify' -import { queryNip05, unixNow } from '../utils' -import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk' import { EventEmitter } from 'tseep' +import { NostrController, relayController } from '.' import { localCache } from '../services' +import { NostrJoiningBlock, ProfileMetadata, RelaySet } from '../types' import { findRelayListAndUpdateCache, findRelayListInCache, getDefaultRelaySet, + getMostPopularRelays, getUserRelaySet, - isOlderThanOneWeek -} from '../utils/relays.ts' + isOlderThanOneWeek, + queryNip05, + unixNow +} from '../utils' export class MetadataController extends EventEmitter { private nostrController: NostrController @@ -51,11 +52,9 @@ export class MetadataController extends EventEmitter { authors: [hexKey] // Authored by the specified key } - const pool = new SimplePool() - // Try to get the metadata event from a special relay (wss://purplepag.es) - const metadataEvent = await pool - .get([this.specialMetadataRelay], eventFilter) + const metadataEvent = await relayController + .fetchEvent(eventFilter, [this.specialMetadataRelay]) .catch((err) => { console.error(err) // Log any errors return null // Return null if an error occurs @@ -80,11 +79,12 @@ export class MetadataController extends EventEmitter { } // If no valid metadata event is found from the special relay, get the most popular relays - const mostPopularRelays = await this.nostrController.getMostPopularRelays() + const mostPopularRelays = await getMostPopularRelays() // Query the most popular relays for metadata events - const events = await pool - .querySync(mostPopularRelays, eventFilter) + + const events = await relayController + .fetchEvents(eventFilter, mostPopularRelays) .catch((err) => { console.error(err) // Log any errors return null // Return null if an error occurs @@ -169,10 +169,7 @@ export class MetadataController extends EventEmitter { [this.specialMetadataRelay], hexKey )) || - (await findRelayListAndUpdateCache( - await this.nostrController.getMostPopularRelays(), - hexKey - )) + (await findRelayListAndUpdateCache(await getMostPopularRelays(), hexKey)) return relayEvent ? getUserRelaySet(relayEvent.tags) : getDefaultRelaySet() } @@ -206,11 +203,15 @@ export class MetadataController extends EventEmitter { await this.nostrController.signEvent(newMetadataEvent) } - await this.nostrController - .publishEvent(signedMetadataEvent, [this.specialMetadataRelay]) + await relayController + .publish(signedMetadataEvent, [this.specialMetadataRelay]) .then((relays) => { - toast.success(`Metadata event published on: ${relays.join('\n')}`) - this.handleNewMetadataEvent(signedMetadataEvent as VerifiedEvent) + if (relays.length) { + toast.success(`Metadata event published on: ${relays.join('\n')}`) + this.handleNewMetadataEvent(signedMetadataEvent as VerifiedEvent) + } else { + toast.error('Could not publish metadata event to any relay!') + } }) .catch((err) => { toast.error(err.message) @@ -250,16 +251,10 @@ export class MetadataController extends EventEmitter { authors: [hexKey] } - const pool = new SimplePool() + // find user's kind 0 event published on user's relays + const event = await relayController.fetchEvent(eventFilter, userRelays) - // find user's kind 0 events published on user's relays - const events = await pool.querySync(userRelays, eventFilter) - if (events && events.length) { - // sort events by created_at time in ascending order - events.sort((a, b) => a.created_at - b.created_at) - - // get first ever event published on user's relays - const event = events[0] + if (event) { const { created_at } = event // initialize job request @@ -283,10 +278,12 @@ export class MetadataController extends EventEmitter { 'wss://relayable.org' ] - // publish job request - await this.nostrController.publishEvent(jobSignedEvent, relays) - - console.log('jobSignedEvent :>> ', jobSignedEvent) + await relayController.publish(jobSignedEvent, relays).catch((err) => { + console.error( + 'Error occurred in publish blockChain-block-number DVM job', + err + ) + }) const subscribeWithTimeout = ( subscription: NDKSubscription, diff --git a/src/controllers/NostrController.ts b/src/controllers/NostrController.ts index 19182b5..cf6c5d6 100644 --- a/src/controllers/NostrController.ts +++ b/src/controllers/NostrController.ts @@ -6,7 +6,6 @@ import NDK, { NDKUser, NostrEvent } from '@nostr-dev-kit/ndk' -import axios from 'axios' import { Event, EventTemplate, @@ -20,10 +19,8 @@ import { nip19, nip44 } from 'nostr-tools' -import { toast } from 'react-toastify' import { EventEmitter } from 'tseep' import { - setMostPopularRelaysAction, setRelayConnectionStatusAction, setRelayInfoAction, updateNsecbunkerPubkey @@ -35,17 +32,17 @@ import { RelayConnectionStatus, RelayInfoObject, RelayMap, - RelayReadStats, - RelayStats, SignedEvent } from '../types' import { compareObjects, + getDefaultRelayMap, + getMostPopularRelays, getNsecBunkerDelegatedKey, unixNow, verifySignedEvent } from '../utils' -import { getDefaultRelayMap } from '../utils/relays.ts' +import { relayController } from './' export class NostrController extends EventEmitter { private static instance: NostrController @@ -223,98 +220,6 @@ export class NostrController extends EventEmitter { return NostrController.instance } - /** - * Function will publish provided event to the provided relays - * - * @param event - The event to publish. - * @param relays - An array of relay URLs to publish the event to. - * @returns A promise that resolves to an array of relays where the event was successfully published. - */ - publishEvent = async (event: Event, relays: string[]) => { - const simplePool = new SimplePool() - - // Publish the event to all relays - const promises = simplePool.publish(relays, event) - - // Use Promise.race to wait for the first successful publish - const firstSuccessfulPublish = await Promise.race( - promises.map((promise, index) => - promise.then(() => relays[index]).catch(() => null) - ) - ) - - if (!firstSuccessfulPublish) { - // If no publish was successful, collect the reasons for failures - const failedPublishes: unknown[] = [] - const fallbackRejectionReason = - 'Attempt to publish an event has been rejected with unknown reason.' - - const results = await Promise.allSettled(promises) - results.forEach((res, index) => { - if (res.status === 'rejected') { - failedPublishes.push({ - relay: relays[index], - error: res.reason - ? res.reason.message || fallbackRejectionReason - : fallbackRejectionReason - }) - } - }) - - throw failedPublishes - } - - // Continue publishing to other relays in the background - promises.forEach((promise, index) => { - promise.catch((err) => { - console.log(`Failed to publish to ${relays[index]}`, err) - }) - }) - - return [firstSuccessfulPublish] - } - - /** - * Asynchronously retrieves an event from a set of relays based on a provided filter. - * If no relays are specified, it defaults to using connected relays. - * - * @param {Filter} filter - The filter criteria to find the event. - * @param {string[]} [relays] - An optional array of relay URLs to search for the event. - * @returns {Promise} - Returns a promise that resolves to the found event or null if not found. - */ - getEvent = async ( - filter: Filter, - relays?: string[] - ): Promise => { - // If no relays are provided or the provided array is empty, use connected relays if available. - if (!relays || relays.length === 0) { - relays = this.connectedRelays - ? this.connectedRelays.map((relay) => relay.url) - : [] - } - - // If still no relays are available, reject the promise with an error message. - if (relays.length === 0) { - return Promise.reject('Provide some relays to find the event') - } - - // Create a new instance of SimplePool to handle the relay connections and event retrieval. - const pool = new SimplePool() - - // Attempt to retrieve the event from the specified relays using the filter criteria. - const event = await pool.get(relays, filter).catch((err) => { - // Log any errors that occur during the event retrieval process. - console.log('An error occurred in finding the event', err) - // Show an error toast notification to the user. - toast.error('An error occurred in finding the event') - // Return null if an error occurs, indicating that no event was found. - return null - }) - - // Return the found event, or null if an error occurred. - return event - } - /** * Encrypts the given content for the specified receiver using NIP-44 encryption. * @@ -659,7 +564,7 @@ export class NostrController extends EventEmitter { getRelayMap = async ( npub: string ): Promise<{ map: RelayMap; mapUpdated?: number }> => { - const mostPopularRelays = await this.getMostPopularRelays() + const mostPopularRelays = await getMostPopularRelays() const pool = new SimplePool() @@ -750,10 +655,13 @@ export class NostrController extends EventEmitter { // If relay map is empty, use most popular relay URIs if (!relaysToPublish.length) { - relaysToPublish = await this.getMostPopularRelays() + relaysToPublish = await getMostPopularRelays() } - const publishResult = await this.publishEvent(signedEvent, relaysToPublish) + const publishResult = await relayController.publish( + signedEvent, + relaysToPublish + ) if (publishResult && publishResult.length) { return Promise.resolve( @@ -764,51 +672,6 @@ export class NostrController extends EventEmitter { return Promise.reject('Publishing updated relay map was unsuccessful.') } - /** - * Provides most popular relays. - * @param numberOfTopRelays - number representing how many most popular relays to provide - * @returns - promise that resolves into an array of most popular relays - */ - getMostPopularRelays = async ( - numberOfTopRelays: number = 30 - ): Promise => { - const mostPopularRelaysState = store.getState().relays?.mostPopular - - // return most popular relays from app state if present - if (mostPopularRelaysState) return mostPopularRelaysState - - // relays in env - const { VITE_MOST_POPULAR_RELAYS } = import.meta.env - const hardcodedPopularRelays = (VITE_MOST_POPULAR_RELAYS || '').split(' ') - const url = `https://stats.nostr.band/stats_api?method=stats` - - const response = await axios.get(url).catch(() => undefined) - - if (!response) { - return hardcodedPopularRelays //return hardcoded relay list - } - - const data = response.data - - if (!data) { - return hardcodedPopularRelays //return hardcoded relay list - } - - const apiTopRelays = data.relay_stats.user_picks.read_relays - .slice(0, numberOfTopRelays) - .map((relay: RelayReadStats) => relay.d) - - if (!apiTopRelays.length) { - return Promise.reject(`Couldn't fetch popular relays.`) - } - - if (store.getState().auth?.loggedIn) { - store.dispatch(setMostPopularRelaysAction(apiTopRelays)) - } - - return apiTopRelays - } - /** * Sets information about relays into relays.info app state. * @param relayURIs - relay URIs to get information about @@ -835,7 +698,7 @@ export class NostrController extends EventEmitter { ] // publish job request - await this.publishEvent(jobSignedEvent, relays) + await relayController.publish(jobSignedEvent, relays) console.log('jobSignedEvent :>> ', jobSignedEvent) diff --git a/src/controllers/RelayController.ts b/src/controllers/RelayController.ts new file mode 100644 index 0000000..1c13cae --- /dev/null +++ b/src/controllers/RelayController.ts @@ -0,0 +1,293 @@ +import { Filter, Relay, Event } from 'nostr-tools' +import { normalizeWebSocketURL, timeout } from '../utils' +import { SIGIT_RELAY } from '../utils/const' + +/** + * Singleton class to manage relay operations. + */ +export class RelayController { + private static instance: RelayController + public connectedRelays: Relay[] = [] + + private constructor() {} + + /** + * Provides the singleton instance of RelayController. + * + * @returns The singleton instance of RelayController. + */ + public static getInstance(): RelayController { + if (!RelayController.instance) { + RelayController.instance = new RelayController() + } + return RelayController.instance + } + + /** + * Connects to a relay server if not already connected. + * + * This method checks if a relay with the given URL is already in the list of connected relays. + * If it is not connected, it attempts to establish a new connection. + * On successful connection, the relay is added to the list of connected relays and returned. + * If the connection fails, an error is logged and `null` is returned. + * + * @param relayUrl - The URL of the relay server to connect to. + * @returns A promise that resolves to the connected relay object if successful, or `null` if the connection fails. + */ + public connectRelay = async (relayUrl: string) => { + // Check if a relay with the same URL is already connected + const relay = this.connectedRelays.find( + (relay) => + normalizeWebSocketURL(relay.url) === normalizeWebSocketURL(relayUrl) + ) + + // If a matching relay is found, return it (skip connection) + if (relay) { + return relay + } + + try { + // Attempt to connect to the relay using the provided URL + const newRelay = await Relay.connect(relayUrl) + + // Add the newly connected relay to the list of connected relays + this.connectedRelays.push(newRelay) + + // Return the newly connected relay + return newRelay + } catch (err) { + // Log an error message if the connection fails + console.error(`Relay connection failed: ${relayUrl}`, err) + + // Return null to indicate connection failure + return null + } + } + + /** + * Asynchronously retrieves multiple event from a set of relays based on a provided filter. + * If no relays are specified, it defaults to using connected relays. + * + * @param {Filter} filter - The filter criteria to find the event. + * @param {string[]} [relays] - An optional array of relay URLs to search for the event. + * @returns {Promise} - Returns a promise that resolves to the found event or null if not found. + */ + fetchEvents = async ( + filter: Filter, + relayUrls: string[] = [] + ): Promise => { + // add app relay to relays array + relayUrls.push(SIGIT_RELAY) + + // Connect to all specified relays + const relayPromises = relayUrls.map((relayUrl) => + 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 + }, []) + + // Check if any relays are connected + if (relays.length === 0) { + throw new Error('No relay is connected to fetch events!') + } + + const events: Event[] = [] + const eventIds = new Set() // To keep track of event IDs and avoid duplicates + + // Create a promise for each relay subscription + const subPromises = relays.map((relay) => { + return new Promise((resolve) => { + // Subscribe to the relay with the specified filter + const sub = relay.subscribe([filter], { + // Handle incoming events + onevent: (e) => { + // Add the event to the array if it's not a duplicate + if (!eventIds.has(e.id)) { + eventIds.add(e.id) // Record the event ID + events.push(e) // Add the event to the array + } + }, + // Handle the End-Of-Stream (EOSE) message + oneose: () => { + sub.close() // Close the subscription + resolve() // Resolve the promise when EOSE is received + } + }) + }) + }) + + // Wait for all subscriptions to complete + await Promise.allSettled(subPromises) + + // It is possible that different relays will send different events and events array may contain more events then specified limit in filter + // To fix this issue we'll first sort these events and then return only limited events + if (filter.limit) { + // Sort events by creation date in descending order + events.sort((a, b) => b.created_at - a.created_at) + + return events.slice(0, filter.limit) + } + + return events + } + + /** + * Asynchronously retrieves an event from a set of relays based on a provided filter. + * If no relays are specified, it defaults to using connected relays. + * + * @param {Filter} filter - The filter criteria to find the event. + * @param {string[]} [relays] - An optional array of relay URLs to search for the event. + * @returns {Promise} - Returns a promise that resolves to the found event or null if not found. + */ + fetchEvent = async ( + filter: Filter, + relays: string[] = [] + ): Promise => { + const events = await this.fetchEvents(filter, relays) + + // Sort events by creation date in descending order + events.sort((a, b) => b.created_at - a.created_at) + + // Return the most recent event, or null if no events were received + return events[0] || null + } + + /** + * Subscribes to events from multiple relays. + * + * This method connects to the specified relay URLs and subscribes to events + * using the provided filter. It handles incoming events through the given + * `eventHandler` callback and manages the subscription lifecycle. + * + * @param filter - The filter criteria to apply when subscribing to events. + * @param relayUrls - An optional array of relay URLs to connect to. The default relay URL (`SIGIT_RELAY`) is added automatically. + * @param eventHandler - A callback function to handle incoming events. It receives an `Event` object. + * + */ + subscribeForEvents = async ( + filter: Filter, + relayUrls: string[] = [], + eventHandler: (event: Event) => void + ) => { + // add app relay to relays array + relayUrls.push(SIGIT_RELAY) + + // Connect to all specified relays + const relayPromises = relayUrls.map((relayUrl) => + 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 + }, []) + + // Check if any relays are connected + if (relays.length === 0) { + throw new Error('No relay is connected to fetch events!') + } + + const processedEvents: string[] = [] // To keep track of processed events + + // Create a promise for each relay subscription + const subPromises = relays.map((relay) => { + return new Promise((resolve) => { + // Subscribe to the relay with the specified filter + const sub = relay.subscribe([filter], { + // Handle incoming events + onevent: (e) => { + // Process event only if it hasn't been processed before + if (!processedEvents.includes(e.id)) { + processedEvents.push(e.id) + eventHandler(e) // Call the event handler with the event + } + }, + // Handle the End-Of-Stream (EOSE) message + oneose: () => { + sub.close() // Close the subscription + resolve() // Resolve the promise when EOSE is received + } + }) + }) + }) + + // Wait for all subscriptions to complete + await Promise.allSettled(subPromises) + } + + publish = async ( + event: Event, + relayUrls: string[] = [] + ): Promise => { + // add app relay to relays array + relayUrls.push(SIGIT_RELAY) + + // Connect to all specified relays + const relayPromises = relayUrls.map((relayUrl) => + 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 + }, []) + + // Check if any relays are connected + if (relays.length === 0) { + throw new Error('No relay is connected to publish event!') + } + + const publishedOnRelays: string[] = [] // List to track which relays successfully published the event + + // Create a promise for publishing the event to each connected relay + const publishPromises = relays.map(async (relay) => { + try { + await Promise.race([ + relay.publish(event), // Publish the event to the relay + timeout(30000) // Set a timeout to handle cases where publishing takes too long + ]) + publishedOnRelays.push(relay.url) // Add the relay URL to the list of successfully published relays + } catch (err) { + console.error(`Failed to publish event on relay: ${relay}`, err) + } + }) + + // Wait for all publish operations to complete (either fulfilled or rejected) + await Promise.allSettled(publishPromises) + + // Return the list of relay URLs where the event was published + return publishedOnRelays + } +} + +export const relayController = RelayController.getInstance() diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 47cba11..dc1f76f 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,4 @@ export * from './AuthController' export * from './MetadataController' export * from './NostrController' +export * from './RelayController' diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index 51c2086..7b34720 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -1,5 +1,5 @@ import { Event, kinds } from 'nostr-tools' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { Outlet } from 'react-router-dom' import { AppBar } from '../components/AppBar/AppBar' @@ -25,7 +25,6 @@ import { subscribeForSigits } from '../utils' import { useAppSelector } from '../hooks' -import { SubCloser } from 'nostr-tools/abstract-pool' import styles from './style.module.scss' import { Footer } from '../components/Footer/Footer' @@ -36,6 +35,9 @@ export const MainLayout = () => { const authState = useSelector((state: State) => state.auth) const usersAppData = useAppSelector((state) => state.userAppData) + // Ref to track if `subscribeForSigits` has been called + const hasSubscribed = useRef(false) + useEffect(() => { const metadataController = new MetadataController() @@ -103,21 +105,15 @@ export const MainLayout = () => { }, [dispatch]) useEffect(() => { - let subCloser: SubCloser | null = null - if (authState.loggedIn && usersAppData) { const pubkey = authState.usersPubkey || authState.keyPair?.public - if (pubkey) { - subscribeForSigits(pubkey).then((res) => { - subCloser = res || null - }) - } - } + if (pubkey && !hasSubscribed.current) { + // Call `subscribeForSigits` only if it hasn't been called before + subscribeForSigits(pubkey) - return () => { - if (subCloser) { - subCloser.close() + // Mark `subscribeForSigits` as called + hasSubscribed.current = true } } }, [authState, usersAppData]) diff --git a/src/utils/index.ts b/src/utils/index.ts index ffac72d..16b36f5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,10 +1,12 @@ export * from './crypto' export * from './hash' export * from './localStorage' -export * from './misc' -export * from './nostr' -export * from './string' -export * from './zip' -export * from './utils' export * from './mark' export * from './meta' +export * from './misc' +export * from './nostr' +export * from './relays' +export * from './string' +export * from './url' +export * from './utils' +export * from './zip' diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index e9ecc8f..48ce930 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -5,7 +5,6 @@ import { Event, EventTemplate, Filter, - SimplePool, UnsignedEvent, finalizeEvent, generateSecretKey, @@ -18,7 +17,11 @@ import { } from 'nostr-tools' import { toast } from 'react-toastify' import { NIP05_REGEX } from '../constants' -import { MetadataController, NostrController } from '../controllers' +import { + MetadataController, + NostrController, + relayController +} from '../controllers' import { updateProcessedGiftWraps, updateUserAppData as updateUserAppDataAction @@ -328,20 +331,27 @@ export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => { } } +/** + * Fetches user application data based on user's public key and stored metadata. + * + * @returns The user application data or null if an error occurs or no data is found. + */ export const getUsersAppData = async (): Promise => { + // Initialize an array to hold relay URLs const relays: string[] = [] + // Retrieve the user's public key and relay map from the Redux store const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const relayMap = store.getState().relays?.map - const nostrController = NostrController.getInstance() - - // check if relaysMap in redux store is undefined + // Check if relayMap is undefined in the Redux store if (!relayMap) { + // If relayMap is not present, fetch relay list metadata const metadataController = new MetadataController() const relaySet = await metadataController .findRelayListMetadata(usersPubkey) .catch((err) => { + // Log error and return null if fetching metadata fails console.log( `An error occurred while finding relay list metadata for ${hexToNpub(usersPubkey)}`, err @@ -349,41 +359,42 @@ export const getUsersAppData = async (): Promise => { return null }) - // Return if metadata retrieval failed + // Return null if metadata retrieval failed if (!relaySet) return null - // Ensure relay list is not empty + // Ensure that the relay list is not empty if (relaySet.write.length === 0) return null + // Add write relays to the relays array relays.push(...relaySet.write) } else { - // filter write relays from user's relayMap stored in redux store + // If relayMap exists, filter and add write relays from the stored map const writeRelays = Object.keys(relayMap).filter( (key) => relayMap[key].write ) - relays.push(...writeRelays) } - // generate an identifier for user's nip78 + // Generate an identifier for the user's nip78 const hash = await getHash('938' + usersPubkey) if (!hash) return null + // Define a filter for fetching events const filter: Filter = { kinds: [kinds.Application], '#d': [hash] } - const encryptedContent = await nostrController - .getEvent(filter, relays) + const encryptedContent = await relayController + .fetchEvent(filter, relays) .then((event) => { if (event) return event.content - // if person is using sigit for first time its possible that event is null - // so we'll return empty stringified object + // If no event is found, return an empty stringified object return '{}' }) .catch((err) => { + // Log error and show a toast notification if fetching event fails console.log(`An error occurred in finding kind 30078 event`, err) toast.error( 'An error occurred in finding kind 30078 event for data storage' @@ -391,8 +402,10 @@ export const getUsersAppData = async (): Promise => { return null }) + // Return null if encrypted content retrieval fails if (!encryptedContent) return null + // Handle case where the encrypted content is an empty object if (encryptedContent === '{}') { const secret = generateSecretKey() const pubKey = getPublicKey(secret) @@ -408,20 +421,28 @@ export const getUsersAppData = async (): Promise => { } } + // Get an instance of the NostrController + const nostrController = NostrController.getInstance() + + // Decrypt the encrypted content const decrypted = await nostrController .nip04Decrypt(usersPubkey, encryptedContent) .catch((err) => { + // Log error and show a toast notification if decryption fails console.log('An error occurred while decrypting app data', err) toast.error('An error occurred while decrypting app data') return null }) + // Return null if decryption fails if (!decrypted) return null + // Parse the decrypted content const parsedContent = await parseJson<{ blossomUrls: string[] keyPair: Keys }>(decrypted).catch((err) => { + // Log error and show a toast notification if parsing fails console.log( 'An error occurred in parsing the content of kind 30078 event', err @@ -430,21 +451,26 @@ export const getUsersAppData = async (): Promise => { return null }) + // Return null if parsing fails if (!parsedContent) return null const { blossomUrls, keyPair } = parsedContent + // Return null if no blossom URLs are found if (blossomUrls.length === 0) return null + // Fetch additional user app data from the first blossom URL const dataFromBlossom = await getUserAppDataFromBlossom( blossomUrls[0], keyPair.private ) + // Return null if fetching data from blossom fails if (!dataFromBlossom) return null const { sigits, processedGiftWraps } = dataFromBlossom + // Return the final user application data return { blossomUrls, keyPair, @@ -575,9 +601,8 @@ export const updateUsersAppData = async (meta: Meta) => { const relayMap = (store.getState().relays as RelaysState).map! const writeRelays = Object.keys(relayMap).filter((key) => relayMap[key].write) - console.log(`publishing event kind: ${kinds.Application}`) const publishResult = await Promise.race([ - nostrController.publishEvent(signedEvent, writeRelays), + relayController.publish(signedEvent, writeRelays), timeout(1000 * 30) ]).catch((err) => { console.log('err :>> ', err) @@ -817,15 +842,8 @@ export const subscribeForSigits = async (pubkey: string) => { '#p': [pubkey] } - // Instantiate a new SimplePool for the subscription - const pool = new SimplePool() - - // Subscribe to the specified relays with the defined filter - return pool.subscribeMany(relaySet.read, [filter], { - // Define a callback function to handle received events - onevent: (event) => { - processReceivedEvent(event) // Process the received event - } + relayController.subscribeForEvents(filter, relaySet.read, (event) => { + processReceivedEvent(event) // Process the received event }) } @@ -915,13 +933,9 @@ export const sendNotification = async (receiver: string, meta: Meta) => { // Ensure relay list is not empty if (relaySet.read.length === 0) return - console.log('Publishing notifications') // Publish the notification event to the recipient's read relays - const nostrController = NostrController.getInstance() - - // Attempt to publish the event to the relays, with a timeout of 2 minutes await Promise.race([ - nostrController.publishEvent(wrappedEvent, relaySet.read), + relayController.publish(wrappedEvent, relaySet.read), timeout(1000 * 30) ]).catch((err) => { // Log an error if publishing the notification event fails diff --git a/src/utils/relays.ts b/src/utils/relays.ts index a7a8227..2d2d8cd 100644 --- a/src/utils/relays.ts +++ b/src/utils/relays.ts @@ -1,9 +1,12 @@ -import { Filter, SimplePool } from 'nostr-tools' +import axios from 'axios' +import { Event, Filter } from 'nostr-tools' import { RelayList } from 'nostr-tools/kinds' -import { Event } from 'nostr-tools' +import { relayController } from '../controllers/RelayController.ts' import { localCache } from '../services' +import { setMostPopularRelaysAction } from '../store/actions' +import store from '../store/store' +import { RelayMap, RelayReadStats, RelaySet, RelayStats } from '../types' import { ONE_WEEK_IN_MS, SIGIT_RELAY } from './const.ts' -import { RelayMap, RelaySet } from '../types' const READ_MARKER = 'read' const WRITE_MARKER = 'write' @@ -24,8 +27,8 @@ const findRelayListAndUpdateCache = async ( kinds: [RelayList], authors: [hexKey] } - const pool = new SimplePool() - const event = await pool.get(lookUpRelays, eventFilter) + + const event = await relayController.fetchEvent(eventFilter, lookUpRelays) if (event) { await localCache.addUserRelayListMetadata(event) } @@ -106,11 +109,57 @@ const toRelaySet = (obj: RelaySet, tag: string[]): RelaySet => { return obj } +/** + * Provides most popular relays. + * @param numberOfTopRelays - number representing how many most popular relays to provide + * @returns - promise that resolves into an array of most popular relays + */ +const getMostPopularRelays = async ( + numberOfTopRelays: number = 30 +): Promise => { + const mostPopularRelaysState = store.getState().relays?.mostPopular + + // return most popular relays from app state if present + if (mostPopularRelaysState) return mostPopularRelaysState + + // relays in env + const { VITE_MOST_POPULAR_RELAYS } = import.meta.env + const hardcodedPopularRelays = (VITE_MOST_POPULAR_RELAYS || '').split(' ') + const url = `https://stats.nostr.band/stats_api?method=stats` + + const response = await axios.get(url).catch(() => undefined) + + if (!response) { + return hardcodedPopularRelays //return hardcoded relay list + } + + const data = response.data + + if (!data) { + return hardcodedPopularRelays //return hardcoded relay list + } + + const apiTopRelays = data.relay_stats.user_picks.read_relays + .slice(0, numberOfTopRelays) + .map((relay: RelayReadStats) => relay.d) + + if (!apiTopRelays.length) { + return Promise.reject(`Couldn't fetch popular relays.`) + } + + if (store.getState().auth?.loggedIn) { + store.dispatch(setMostPopularRelaysAction(apiTopRelays)) + } + + return apiTopRelays +} + export { findRelayListAndUpdateCache, findRelayListInCache, - getUserRelaySet, - getDefaultRelaySet, getDefaultRelayMap, + getDefaultRelaySet, + getMostPopularRelays, + getUserRelaySet, isOlderThanOneWeek } diff --git a/src/utils/url.ts b/src/utils/url.ts new file mode 100644 index 0000000..2b8181a --- /dev/null +++ b/src/utils/url.ts @@ -0,0 +1,47 @@ +/** + * Normalizes a given URL by performing the following operations: + * + * 1. Ensures that the URL has a protocol by defaulting to 'wss://' if no protocol is provided. + * 2. Creates a `URL` object to easily manipulate and normalize the URL components. + * 3. Normalizes the pathname by: + * - Replacing multiple consecutive slashes with a single slash. + * - Removing the trailing slash if it exists. + * 4. Removes the port number if it is the default port for the protocol: + * - Port `80` for 'ws:' (WebSocket) protocol. + * - Port `443` for 'wss:' (WebSocket Secure) protocol. + * 5. Sorts the query parameters alphabetically. + * 6. Clears any fragment (hash) identifier from the URL. + * + * @param urlString - The URL string to be normalized. + * @returns A normalized URL string. + */ +export function normalizeWebSocketURL(urlString: string): string { + // If the URL string does not contain a protocol (e.g., "http://", "https://"), + // prepend "wss://" (WebSocket Secure) by default. + if (urlString.indexOf('://') === -1) urlString = 'wss://' + urlString + + // Create a URL object from the provided URL string. + const url = new URL(urlString) + + // Normalize the pathname by replacing multiple consecutive slashes with a single slash. + url.pathname = url.pathname.replace(/\/+/g, '/') + + // Remove the trailing slash from the pathname if it exists. + if (url.pathname.endsWith('/')) url.pathname = url.pathname.slice(0, -1) + + // Remove the port number if it is 80 for "ws:" protocol or 443 for "wss:" protocol, as these are default ports. + if ( + (url.port === '80' && url.protocol === 'ws:') || + (url.port === '443' && url.protocol === 'wss:') + ) + url.port = '' + + // Sort the search parameters alphabetically. + url.searchParams.sort() + + // Clear any hash fragment from the URL. + url.hash = '' + + // Return the normalized URL as a string. + return url.toString() +} From 03cb6b1732324aff887400f2dd8be9857028e4ef Mon Sep 17 00:00:00 2001 From: daniyal Date: Fri, 16 Aug 2024 11:42:28 +0500 Subject: [PATCH 26/63] chore(refactor): move getNostrJoiningBlockNumber function to a separate dvm utils file --- src/controllers/MetadataController.ts | 128 +----------------------- src/pages/profile/index.tsx | 10 +- src/pages/settings/profile/index.tsx | 9 +- src/utils/dvm.ts | 135 ++++++++++++++++++++++++++ src/utils/index.ts | 1 + 5 files changed, 150 insertions(+), 133 deletions(-) create mode 100644 src/utils/dvm.ts diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index b9557bb..bf1b3d8 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -1,11 +1,8 @@ -import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk' import { Event, - EventTemplate, Filter, VerifiedEvent, kinds, - nip19, validateEvent, verifyEvent } from 'nostr-tools' @@ -13,7 +10,7 @@ import { toast } from 'react-toastify' import { EventEmitter } from 'tseep' import { NostrController, relayController } from '.' import { localCache } from '../services' -import { NostrJoiningBlock, ProfileMetadata, RelaySet } from '../types' +import { ProfileMetadata, RelaySet } from '../types' import { findRelayListAndUpdateCache, findRelayListInCache, @@ -21,7 +18,6 @@ import { getMostPopularRelays, getUserRelaySet, isOlderThanOneWeek, - queryNip05, unixNow } from '../utils' @@ -218,128 +214,6 @@ export class MetadataController extends EventEmitter { }) } - public getNostrJoiningBlockNumber = async ( - hexKey: string - ): Promise => { - const relaySet = await this.findRelayListMetadata(hexKey) - - const userRelays: string[] = [] - - // find user's relays - if (relaySet.write.length > 0) { - userRelays.push(...relaySet.write) - } else { - const metadata = await this.findMetadata(hexKey) - if (!metadata) return null - - const metadataContent = this.extractProfileMetadataContent(metadata) - - if (metadataContent?.nip05) { - const nip05Profile = await queryNip05(metadataContent.nip05) - - if (nip05Profile && nip05Profile.pubkey === hexKey) { - userRelays.push(...nip05Profile.relays) - } - } - } - - if (userRelays.length === 0) return null - - // filter for finding user's first kind 0 event - const eventFilter: Filter = { - kinds: [kinds.Metadata], - authors: [hexKey] - } - - // find user's kind 0 event published on user's relays - const event = await relayController.fetchEvent(eventFilter, userRelays) - - if (event) { - const { created_at } = event - - // initialize job request - const jobEventTemplate: EventTemplate = { - content: '', - created_at: unixNow(), - kind: 68001, - tags: [ - ['i', `${created_at * 1000}`], - ['j', 'blockChain-block-number'] - ] - } - - // sign job request event - const jobSignedEvent = - await this.nostrController.signEvent(jobEventTemplate) - - const relays = [ - 'wss://relay.damus.io', - 'wss://relay.primal.net', - 'wss://relayable.org' - ] - - await relayController.publish(jobSignedEvent, relays).catch((err) => { - console.error( - 'Error occurred in publish blockChain-block-number DVM job', - err - ) - }) - - const subscribeWithTimeout = ( - subscription: NDKSubscription, - timeoutMs: number - ): Promise => { - return new Promise((resolve, reject) => { - const eventHandler = (event: NDKEvent) => { - subscription.stop() - resolve(event.content) - } - - subscription.on('event', eventHandler) - - // Set up a timeout to stop the subscription after a specified time - const timeout = setTimeout(() => { - subscription.stop() // Stop the subscription - reject(new Error('Subscription timed out')) // Reject the promise with a timeout error - }, timeoutMs) - - // Handle subscription close event - subscription.on('close', () => clearTimeout(timeout)) - }) - } - - const dvmNDK = new NDK({ - explicitRelayUrls: relays - }) - - await dvmNDK.connect(2000) - - // filter for getting DVM job's result - const sub = dvmNDK.subscribe({ - kinds: [68002 as number], - '#e': [jobSignedEvent.id], - '#p': [jobSignedEvent.pubkey] - }) - - // asynchronously get block number from dvm job with 20 seconds timeout - const dvmJobResult = await subscribeWithTimeout(sub, 20000) - - const encodedEventPointer = nip19.neventEncode({ - id: event.id, - relays: userRelays, - author: event.pubkey, - kind: event.kind - }) - - return { - block: parseInt(dvmJobResult), - encodedEventPointer - } - } - - return null - } - public validate = (event: Event) => validateEvent(event) && verifyEvent(event) public getEmptyMetadataEvent = (): Event => { diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index c1822b9..a7b205b 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -12,7 +12,12 @@ import { MetadataController } from '../../controllers' import { getProfileSettingsRoute } from '../../routes' import { State } from '../../store/rootReducer' import { NostrJoiningBlock, ProfileMetadata } from '../../types' -import { getRoboHashPicture, hexToNpub, shorten } from '../../utils' +import { + getNostrJoiningBlockNumber, + getRoboHashPicture, + hexToNpub, + shorten +} from '../../utils' import styles from './style.module.scss' import { Container } from '../../components/Container' @@ -51,8 +56,7 @@ export const ProfilePage = () => { useEffect(() => { if (pubkey) { - metadataController - .getNostrJoiningBlockNumber(pubkey) + getNostrJoiningBlockNumber(pubkey) .then((res) => { setNostrJoiningBlock(res) }) diff --git a/src/pages/settings/profile/index.tsx b/src/pages/settings/profile/index.tsx index 9b2fc2d..8723c2e 100644 --- a/src/pages/settings/profile/index.tsx +++ b/src/pages/settings/profile/index.tsx @@ -26,7 +26,11 @@ import { setMetadataEvent } from '../../../store/actions' import { LoadingSpinner } from '../../../components/LoadingSpinner' import { LoginMethods } from '../../../store/auth/types' import { SmartToy } from '@mui/icons-material' -import { getRoboHashPicture, unixNow } from '../../../utils' +import { + getNostrJoiningBlockNumber, + getRoboHashPicture, + unixNow +} from '../../../utils' import { Container } from '../../../components/Container' export const ProfileSettingsPage = () => { @@ -71,8 +75,7 @@ export const ProfileSettingsPage = () => { useEffect(() => { if (pubkey) { - metadataController - .getNostrJoiningBlockNumber(pubkey) + getNostrJoiningBlockNumber(pubkey) .then((res) => { setNostrJoiningBlock(res) }) diff --git a/src/utils/dvm.ts b/src/utils/dvm.ts new file mode 100644 index 0000000..ba985ed --- /dev/null +++ b/src/utils/dvm.ts @@ -0,0 +1,135 @@ +import { EventTemplate, Filter, kinds, nip19 } from 'nostr-tools' +import { queryNip05, unixNow } from '.' +import { + MetadataController, + NostrController, + relayController +} from '../controllers' +import { NostrJoiningBlock } from '../types' +import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk' + +export const getNostrJoiningBlockNumber = async ( + hexKey: string +): Promise => { + const metadataController = new MetadataController() + + const relaySet = await metadataController.findRelayListMetadata(hexKey) + + const userRelays: string[] = [] + + // find user's relays + if (relaySet.write.length > 0) { + userRelays.push(...relaySet.write) + } else { + const metadata = await metadataController.findMetadata(hexKey) + if (!metadata) return null + + const metadataContent = + metadataController.extractProfileMetadataContent(metadata) + + if (metadataContent?.nip05) { + const nip05Profile = await queryNip05(metadataContent.nip05) + + if (nip05Profile && nip05Profile.pubkey === hexKey) { + userRelays.push(...nip05Profile.relays) + } + } + } + + if (userRelays.length === 0) return null + + // filter for finding user's first kind 0 event + const eventFilter: Filter = { + kinds: [kinds.Metadata], + authors: [hexKey] + } + + // find user's kind 0 event published on user's relays + const event = await relayController.fetchEvent(eventFilter, userRelays) + + if (event) { + const { created_at } = event + + // initialize job request + const jobEventTemplate: EventTemplate = { + content: '', + created_at: unixNow(), + kind: 68001, + tags: [ + ['i', `${created_at * 1000}`], + ['j', 'blockChain-block-number'] + ] + } + + const nostrController = NostrController.getInstance() + + // sign job request event + const jobSignedEvent = await nostrController.signEvent(jobEventTemplate) + + const relays = [ + 'wss://relay.damus.io', + 'wss://relay.primal.net', + 'wss://relayable.org' + ] + + await relayController.publish(jobSignedEvent, relays).catch((err) => { + console.error( + 'Error occurred in publish blockChain-block-number DVM job', + err + ) + }) + + const subscribeWithTimeout = ( + subscription: NDKSubscription, + timeoutMs: number + ): Promise => { + return new Promise((resolve, reject) => { + const eventHandler = (event: NDKEvent) => { + subscription.stop() + resolve(event.content) + } + + subscription.on('event', eventHandler) + + // Set up a timeout to stop the subscription after a specified time + const timeout = setTimeout(() => { + subscription.stop() // Stop the subscription + reject(new Error('Subscription timed out')) // Reject the promise with a timeout error + }, timeoutMs) + + // Handle subscription close event + subscription.on('close', () => clearTimeout(timeout)) + }) + } + + const dvmNDK = new NDK({ + explicitRelayUrls: relays + }) + + await dvmNDK.connect(2000) + + // filter for getting DVM job's result + const sub = dvmNDK.subscribe({ + kinds: [68002 as number], + '#e': [jobSignedEvent.id], + '#p': [jobSignedEvent.pubkey] + }) + + // asynchronously get block number from dvm job with 20 seconds timeout + const dvmJobResult = await subscribeWithTimeout(sub, 20000) + + const encodedEventPointer = nip19.neventEncode({ + id: event.id, + relays: userRelays, + author: event.pubkey, + kind: event.kind + }) + + return { + block: parseInt(dvmJobResult), + encodedEventPointer + } + } + + return null +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 16b36f5..accc008 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './crypto' +export * from './dvm' export * from './hash' export * from './localStorage' export * from './mark' From 66c7182fa4e0ec1f18c8bea7e937dcd5accfd262 Mon Sep 17 00:00:00 2001 From: daniyal Date: Sun, 18 Aug 2024 22:48:48 +0500 Subject: [PATCH 27/63] chore(refactor): move dvm related function to dvm utils file and relay related to relays utils file --- src/controllers/AuthController.ts | 9 +- src/controllers/NostrController.ts | 345 +---------------- src/controllers/RelayController.ts | 63 ++-- src/hooks/index.ts | 1 + src/hooks/useDidMount.ts | 12 + src/pages/settings/relays/index.tsx | 564 ++++++++++++---------------- src/utils/dvm.ts | 97 ++++- src/utils/nostr.ts | 4 +- src/utils/relays.ts | 126 ++++++- 9 files changed, 517 insertions(+), 704 deletions(-) create mode 100644 src/hooks/useDidMount.ts diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 33f5c82..09b20df 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -1,22 +1,23 @@ import { EventTemplate } from 'nostr-tools' import { MetadataController, NostrController } from '.' +import { appPrivateRoutes } from '../routes' import { setAuthState, setMetadataEvent, setRelayMapAction } from '../store/actions' import store from '../store/store' +import { SignedEvent } from '../types' import { base64DecodeAuthToken, base64EncodeSignedEvent, + compareObjects, getAuthToken, + getRelayMap, getVisitedLink, saveAuthToken, - compareObjects, unixNow } from '../utils' -import { appPrivateRoutes } from '../routes' -import { SignedEvent } from '../types' export class AuthController { private nostrController: NostrController @@ -75,7 +76,7 @@ export class AuthController { }) ) - const relayMap = await this.nostrController.getRelayMap(pubkey) + const relayMap = await getRelayMap(pubkey) if (Object.keys(relayMap).length < 1) { // Navigate user to relays page if relay map is empty diff --git a/src/controllers/NostrController.ts b/src/controllers/NostrController.ts index cf6c5d6..0547ffb 100644 --- a/src/controllers/NostrController.ts +++ b/src/controllers/NostrController.ts @@ -2,47 +2,24 @@ import NDK, { NDKEvent, NDKNip46Signer, NDKPrivateKeySigner, - NDKSubscription, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk' import { Event, EventTemplate, - Filter, - Relay, - SimplePool, UnsignedEvent, finalizeEvent, - kinds, nip04, nip19, nip44 } from 'nostr-tools' import { EventEmitter } from 'tseep' -import { - setRelayConnectionStatusAction, - setRelayInfoAction, - updateNsecbunkerPubkey -} from '../store/actions' +import { updateNsecbunkerPubkey } from '../store/actions' import { AuthState, LoginMethods } from '../store/auth/types' import store from '../store/store' -import { - RelayConnectionState, - RelayConnectionStatus, - RelayInfoObject, - RelayMap, - SignedEvent -} from '../types' -import { - compareObjects, - getDefaultRelayMap, - getMostPopularRelays, - getNsecBunkerDelegatedKey, - unixNow, - verifySignedEvent -} from '../utils' -import { relayController } from './' +import { SignedEvent } from '../types' +import { getNsecBunkerDelegatedKey, verifySignedEvent } from '../utils' export class NostrController extends EventEmitter { private static instance: NostrController @@ -50,14 +27,13 @@ export class NostrController extends EventEmitter { private bunkerNDK: NDK | undefined private remoteSigner: NDKNip46Signer | undefined - private connectedRelays: Relay[] | undefined - private constructor() { super() } private getNostrObject = () => { // fix: this is not picking up type declaration from src/system/index.d.ts + // eslint-disable-next-line @typescript-eslint/no-explicit-any if (window.nostr) return window.nostr as any throw new Error( @@ -555,317 +531,4 @@ export class NostrController extends EventEmitter { generateDelegatedKey = (): string => { return NDKPrivateKeySigner.generate().privateKey! } - - /** - * Provides relay map. - * @param npub - user's npub - * @returns - promise that resolves into relay map and a timestamp when it has been updated. - */ - getRelayMap = async ( - npub: string - ): Promise<{ map: RelayMap; mapUpdated?: number }> => { - const mostPopularRelays = await getMostPopularRelays() - - const pool = new SimplePool() - - // More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md - const eventFilter: Filter = { - kinds: [kinds.RelayList], - authors: [npub] - } - - const event = await pool - .get(mostPopularRelays, eventFilter) - .catch((err) => { - return Promise.reject(err) - }) - - if (event) { - // Handle founded 10002 event - const relaysMap: RelayMap = {} - - // 'r' stands for 'relay' - const relayTags = event.tags.filter((tag) => tag[0] === 'r') - - relayTags.forEach((tag) => { - const uri = tag[1] - const relayType = tag[2] - - // if 3rd element of relay tag is undefined, relay is WRITE and READ - relaysMap[uri] = { - write: relayType ? relayType === 'write' : true, - read: relayType ? relayType === 'read' : true - } - }) - - this.getRelayInfo(Object.keys(relaysMap)) - - this.connectToRelays(Object.keys(relaysMap)) - - return Promise.resolve({ map: relaysMap, mapUpdated: event.created_at }) - } else { - return Promise.resolve({ map: getDefaultRelayMap() }) - } - } - - /** - * Publishes relay map. - * @param relayMap - relay map. - * @param npub - user's npub. - * @param extraRelaysToPublish - optional relays to publish relay map. - * @returns - promise that resolves into a string representing publishing result. - */ - publishRelayMap = async ( - relayMap: RelayMap, - npub: string, - extraRelaysToPublish?: string[] - ): Promise => { - const timestamp = unixNow() - const relayURIs = Object.keys(relayMap) - - // More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md - const tags: string[][] = relayURIs.map((relayURI) => - [ - 'r', - relayURI, - relayMap[relayURI].read && relayMap[relayURI].write - ? '' - : relayMap[relayURI].write - ? 'write' - : 'read' - ].filter((value) => value !== '') - ) - - const newRelayMapEvent: UnsignedEvent = { - kind: kinds.RelayList, - tags, - content: '', - pubkey: npub, - created_at: timestamp - } - - const signedEvent = await this.signEvent(newRelayMapEvent) - - let relaysToPublish = relayURIs - - // Add extra relays if provided - if (extraRelaysToPublish) { - relaysToPublish = [...relaysToPublish, ...extraRelaysToPublish] - } - - // If relay map is empty, use most popular relay URIs - if (!relaysToPublish.length) { - relaysToPublish = await getMostPopularRelays() - } - - const publishResult = await relayController.publish( - signedEvent, - relaysToPublish - ) - - if (publishResult && publishResult.length) { - return Promise.resolve( - `Relay Map published on: ${publishResult.join('\n')}` - ) - } - - return Promise.reject('Publishing updated relay map was unsuccessful.') - } - - /** - * Sets information about relays into relays.info app state. - * @param relayURIs - relay URIs to get information about - */ - getRelayInfo = async (relayURIs: string[]) => { - // initialize job request - const jobEventTemplate: EventTemplate = { - content: '', - created_at: unixNow(), - kind: 68001, - tags: [ - ['i', `${JSON.stringify(relayURIs)}`], - ['j', 'relay-info'] - ] - } - - // sign job request event - const jobSignedEvent = await this.signEvent(jobEventTemplate) - - const relays = [ - 'wss://relay.damus.io', - 'wss://relay.primal.net', - 'wss://relayable.org' - ] - - // publish job request - await relayController.publish(jobSignedEvent, relays) - - console.log('jobSignedEvent :>> ', jobSignedEvent) - - const subscribeWithTimeout = ( - subscription: NDKSubscription, - timeoutMs: number - ): Promise => { - return new Promise((resolve, reject) => { - const eventHandler = (event: NDKEvent) => { - subscription.stop() - resolve(event.content) - } - - subscription.on('event', eventHandler) - - // Set up a timeout to stop the subscription after a specified time - const timeout = setTimeout(() => { - subscription.stop() // Stop the subscription - reject(new Error('Subscription timed out')) // Reject the promise with a timeout error - }, timeoutMs) - - // Handle subscription close event - subscription.on('close', () => clearTimeout(timeout)) - }) - } - - const dvmNDK = new NDK({ - explicitRelayUrls: relays - }) - - await dvmNDK.connect(2000) - - // filter for getting DVM job's result - const sub = dvmNDK.subscribe({ - kinds: [68002 as number], - '#e': [jobSignedEvent.id], - '#p': [jobSignedEvent.pubkey] - }) - - // asynchronously get block number from dvm job with 20 seconds timeout - const dvmJobResult = await subscribeWithTimeout(sub, 20000) - - if (!dvmJobResult) { - return Promise.reject(`Relay(s) information wasn't received`) - } - - let relaysInfo: RelayInfoObject - - try { - relaysInfo = JSON.parse(dvmJobResult) - } catch (error) { - return Promise.reject(`Invalid relay(s) information.`) - } - - if ( - relaysInfo && - !compareObjects(store.getState().relays?.info, relaysInfo) - ) { - store.dispatch(setRelayInfoAction(relaysInfo)) - } - } - - /** - * Establishes connection to relays. - * @param relayURIs - an array of relay URIs - * @returns - promise that resolves into an array of connections - */ - connectToRelays = async (relayURIs: string[]) => { - // Copy of relay connection status - const relayConnectionsStatus: RelayConnectionStatus = JSON.parse( - JSON.stringify(store.getState().relays?.connectionStatus || {}) - ) - - const connectedRelayURLs = this.connectedRelays - ? this.connectedRelays.map((relay) => relay.url) - : [] - - // Check if connections already established - if (compareObjects(connectedRelayURLs, relayURIs)) { - return - } - - const connections = relayURIs - .filter((relayURI) => !connectedRelayURLs.includes(relayURI)) - .map((relayURI) => - Relay.connect(relayURI) - .then((relay) => { - // put connection status into relayConnectionsStatus object - relayConnectionsStatus[relayURI] = relay.connected - ? RelayConnectionState.Connected - : RelayConnectionState.NotConnected - - return relay - }) - .catch(() => { - relayConnectionsStatus[relayURI] = RelayConnectionState.NotConnected - }) - ) - - const connected = await Promise.all(connections) - - // put connected relays into connectedRelays private property, so it can be closed later - this.connectedRelays = connected.filter( - (relay) => relay instanceof Relay && relay.connected - ) as Relay[] - - if (Object.keys(relayConnectionsStatus)) { - if ( - !compareObjects( - store.getState().relays?.connectionStatus, - relayConnectionsStatus - ) - ) { - store.dispatch(setRelayConnectionStatusAction(relayConnectionsStatus)) - } - } - - return Promise.resolve(relayConnectionsStatus) - } - - /** - * Disconnects from relays. - * @param relayURIs - array of relay URIs to disconnect from - */ - disconnectFromRelays = async (relayURIs: string[]) => { - const connectedRelayURLs = this.connectedRelays - ? this.connectedRelays.map((relay) => relay.url) - : [] - - relayURIs - .filter((relayURI) => connectedRelayURLs.includes(relayURI)) - .forEach((relayURI) => { - if (this.connectedRelays) { - const relay = this.connectedRelays.find( - (relay) => relay.url === relayURI - ) - - if (relay) { - // close relay connection - relay.close() - - // remove relay from connectedRelays property - this.connectedRelays = this.connectedRelays.filter( - (relay) => relay.url !== relayURI - ) - } - } - }) - - if (store.getState().relays?.connectionStatus) { - const connectionStatus = JSON.parse( - JSON.stringify(store.getState().relays?.connectionStatus) - ) - - relayURIs.forEach((relay) => { - delete connectionStatus[relay] - }) - - if ( - !compareObjects( - store.getState().relays?.connectionStatus, - connectionStatus - ) - ) { - // Update app state - store.dispatch(setRelayConnectionStatusAction(connectionStatus)) - } - } - } } diff --git a/src/controllers/RelayController.ts b/src/controllers/RelayController.ts index 1c13cae..4ab711c 100644 --- a/src/controllers/RelayController.ts +++ b/src/controllers/RelayController.ts @@ -1,4 +1,4 @@ -import { Filter, Relay, Event } from 'nostr-tools' +import { Event, Filter, Relay } from 'nostr-tools' import { normalizeWebSocketURL, timeout } from '../utils' import { SIGIT_RELAY } from '../utils/const' @@ -7,7 +7,7 @@ import { SIGIT_RELAY } from '../utils/const' */ export class RelayController { private static instance: RelayController - public connectedRelays: Relay[] = [] + public connectedRelays = new Map() private constructor() {} @@ -34,34 +34,44 @@ export class RelayController { * @param relayUrl - The URL of the relay server to connect to. * @returns A promise that resolves to the connected relay object if successful, or `null` if the connection fails. */ - public connectRelay = async (relayUrl: string) => { + public connectRelay = async (relayUrl: string): Promise => { // Check if a relay with the same URL is already connected - const relay = this.connectedRelays.find( - (relay) => - normalizeWebSocketURL(relay.url) === normalizeWebSocketURL(relayUrl) - ) + const normalizedWebSocketURL = normalizeWebSocketURL(relayUrl) + const relay = this.connectedRelays.get(normalizedWebSocketURL) - // If a matching relay is found, return it (skip connection) if (relay) { - return relay + // If a relay is found in connectedRelay map and is connected, just return it + if (relay.connected) return relay + + // If relay is found in connectedRelay map but not connected, + // remove it from map and call connectRelay method again + this.connectedRelays.delete(relayUrl) + + return this.connectRelay(relayUrl) } - try { - // Attempt to connect to the relay using the provided URL - const newRelay = await Relay.connect(relayUrl) + // Attempt to connect to the relay using the provided URL + const newRelay = await Relay.connect(relayUrl) + .then((relay) => { + if (relay.connected) { + // Add the newly connected relay to the connected relays map + this.connectedRelays.set(relayUrl, relay) - // Add the newly connected relay to the list of connected relays - this.connectedRelays.push(newRelay) + // Return the newly connected relay + return relay + } - // Return the newly connected relay - return newRelay - } catch (err) { - // Log an error message if the connection fails - console.error(`Relay connection failed: ${relayUrl}`, err) + return null + }) + .catch((err) => { + // Log an error message if the connection fails + console.error(`Relay connection failed: ${relayUrl}`, err) - // Return null to indicate connection failure - return null - } + // Return null to indicate connection failure + return null + }) + + return newRelay } /** @@ -109,6 +119,11 @@ export class RelayController { // Create a promise for each relay subscription const subPromises = relays.map((relay) => { return new Promise((resolve) => { + if (!relay.connected) { + console.log(`${relay.url} : Not connected!`, 'Skipping subscription') + return + } + // Subscribe to the relay with the specified filter const sub = relay.subscribe([filter], { // Handle incoming events @@ -274,11 +289,11 @@ export class RelayController { try { await Promise.race([ relay.publish(event), // Publish the event to the relay - timeout(30000) // Set a timeout to handle cases where publishing takes too long + timeout(20 * 1000) // Set a timeout to handle cases where publishing takes too long ]) publishedOnRelays.push(relay.url) // Add the relay URL to the list of successfully published relays } catch (err) { - console.error(`Failed to publish event on relay: ${relay}`, err) + console.error(`Failed to publish event on relay: ${relay.url}`, err) } }) diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 16c8633..e7ec305 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,2 @@ export * from './store' +export * from './useDidMount' diff --git a/src/hooks/useDidMount.ts b/src/hooks/useDidMount.ts new file mode 100644 index 0000000..5bac96a --- /dev/null +++ b/src/hooks/useDidMount.ts @@ -0,0 +1,12 @@ +import { useRef, useEffect } from 'react' + +export const useDidMount = (callback: () => void) => { + const didMount = useRef(false) + + useEffect(() => { + if (callback && !didMount.current) { + didMount.current = true + callback() + } + }) +} diff --git a/src/pages/settings/relays/index.tsx b/src/pages/settings/relays/index.tsx index 929a093..73e8c6f 100644 --- a/src/pages/settings/relays/index.tsx +++ b/src/pages/settings/relays/index.tsx @@ -12,138 +12,41 @@ import ListItemText from '@mui/material/ListItemText' import Switch from '@mui/material/Switch' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' -import { NostrController } from '../../../controllers' -import { useAppDispatch, useAppSelector } from '../../../hooks' -import { - setRelayMapAction, - setRelayMapUpdatedAction -} from '../../../store/actions' -import { - RelayConnectionState, - RelayFee, - RelayInfoObject, - RelayMap -} from '../../../types' +import { Container } from '../../../components/Container' +import { relayController } from '../../../controllers' +import { useAppDispatch, useAppSelector, useDidMount } from '../../../hooks' +import { setRelayMapAction } from '../../../store/actions' +import { RelayConnectionState, RelayFee, RelayInfo } from '../../../types' import { capitalizeFirstLetter, - compareObjects, + getRelayInfo, + getRelayMap, hexToNpub, + publishRelayMap, shorten } from '../../../utils' import styles from './style.module.scss' -import { Container } from '../../../components/Container' export const RelaysPage = () => { - const nostrController = NostrController.getInstance() - - const relaysState = useAppSelector((state) => state.relays) const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey) const dispatch = useAppDispatch() const [newRelayURI, setNewRelayURI] = useState() const [newRelayURIerror, setNewRelayURIerror] = useState() - const [relayMap, setRelayMap] = useState( - relaysState?.map - ) - const [relaysInfo, setRelaysInfo] = useState( - relaysState?.info - ) - const [displayRelaysInfo, setDisplayRelaysInfo] = useState([]) - const [relaysConnectionStatus, setRelaysConnectionStatus] = useState( - relaysState?.connectionStatus - ) + + const relayMap = useAppSelector((state) => state.relays?.map) + const relaysInfo = useAppSelector((state) => state.relays?.info) const webSocketPrefix = 'wss://' - // Update relay connection status - useEffect(() => { - if ( - !compareObjects(relaysConnectionStatus, relaysState?.connectionStatus) - ) { - setRelaysConnectionStatus(relaysState?.connectionStatus) + useDidMount(() => { + if (usersPubkey) { + getRelayMap(usersPubkey).then((newRelayMap) => { + dispatch(setRelayMapAction(newRelayMap.map)) + }) } - }, [relaysConnectionStatus, relaysState?.connectionStatus]) - - useEffect(() => { - if (!compareObjects(relaysInfo, relaysState?.info)) { - setRelaysInfo(relaysState?.info) - } - }, [relaysInfo, relaysState?.info]) - - useEffect(() => { - if (!compareObjects(relayMap, relaysState?.map)) { - setRelayMap(relaysState?.map) - } - }, [relayMap, relaysState?.map]) - - useEffect(() => { - let isMounted = false - - const fetchData = async () => { - if (usersPubkey) { - isMounted = true - - // call async func to fetch relay map - const newRelayMap = await nostrController.getRelayMap(usersPubkey) - - // handle fetched relay map - if (isMounted) { - if ( - !relaysState?.mapUpdated || - (newRelayMap?.mapUpdated !== undefined && - newRelayMap?.mapUpdated > relaysState?.mapUpdated) - ) { - if ( - !relaysState?.map || - !compareObjects(relaysState.map, newRelayMap) - ) { - setRelayMap(newRelayMap.map) - - dispatch(setRelayMapAction(newRelayMap.map)) - } else { - // Update relay map updated timestamp - dispatch(setRelayMapUpdatedAction()) - } - } - } - } - } - - // Publishing relay map can take some time. - // This is why data fetch should happen only if relay map was received more than 5 minutes ago. - if ( - usersPubkey && - (!relaysState?.mapUpdated || - Date.now() - relaysState?.mapUpdated > 5 * 60 * 1000) // 5 minutes - ) { - fetchData() - - // Update relay connection status - if (relaysConnectionStatus) { - const notConnectedRelays = Object.keys(relaysConnectionStatus).filter( - (key) => - relaysConnectionStatus[key] === RelayConnectionState.NotConnected - ) - - if (notConnectedRelays.length) { - nostrController.connectToRelays(notConnectedRelays) - } - } - } - - // cleanup func - return () => { - isMounted = false - } - }, [ - dispatch, - usersPubkey, - relaysState?.map, - relaysState?.mapUpdated, - nostrController, - relaysConnectionStatus - ]) + }) useEffect(() => { // Display notification if an empty relay map has been received @@ -175,24 +78,23 @@ export const RelaysPage = () => { if (usersPubkey) { // Publish updated relay map. - const relayMapPublishingRes = await nostrController - .publishRelayMap(relayMapCopy, usersPubkey, [relay]) - .catch((err) => handlePublishRelayMapError(err)) + const relayMapPublishingRes = await publishRelayMap( + relayMapCopy, + usersPubkey, + [relay] + ).catch((err) => handlePublishRelayMapError(err)) if (relayMapPublishingRes) { toast.success(relayMapPublishingRes) - setRelayMap(relayMapCopy) - dispatch(setRelayMapAction(relayMapCopy)) } } - - nostrController.disconnectFromRelays([relay]) } } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any const handlePublishRelayMapError = (err: any) => { const errorPrefix = 'Error while publishing Relay Map' @@ -224,15 +126,14 @@ export const RelaysPage = () => { if (usersPubkey) { // Publish updated relay map - const relayMapPublishingRes = await nostrController - .publishRelayMap(relayMapCopy, usersPubkey) - .catch((err) => handlePublishRelayMapError(err)) + const relayMapPublishingRes = await publishRelayMap( + relayMapCopy, + usersPubkey + ).catch((err) => handlePublishRelayMapError(err)) if (relayMapPublishingRes) { toast.success(relayMapPublishingRes) - setRelayMap(relayMapCopy) - dispatch(setRelayMapAction(relayMapCopy)) } } @@ -256,29 +157,25 @@ export const RelaysPage = () => { ) } } else if (relayURI && usersPubkey) { - const connectionStatus = await nostrController.connectToRelays([relayURI]) + const relay = await relayController.connectRelay(relayURI) - if ( - connectionStatus && - connectionStatus[relayURI] && - connectionStatus[relayURI] === RelayConnectionState.Connected - ) { + if (relay && relay.connected) { const relayMapCopy = JSON.parse(JSON.stringify(relayMap)) relayMapCopy[relayURI] = { write: true, read: true } // Publish updated relay map - const relayMapPublishingRes = await nostrController - .publishRelayMap(relayMapCopy, usersPubkey) - .catch((err) => handlePublishRelayMapError(err)) + const relayMapPublishingRes = await publishRelayMap( + relayMapCopy, + usersPubkey + ).catch((err) => handlePublishRelayMapError(err)) if (relayMapPublishingRes) { - setRelayMap(relayMapCopy) setNewRelayURI('') dispatch(setRelayMapAction(relayMapCopy)) - nostrController.getRelayInfo([relayURI]) + getRelayInfo([relayURI]) toast.success(relayMapPublishingRes) } @@ -292,29 +189,6 @@ export const RelaysPage = () => { } } - // Handle relay open and close state - const handleRelayInfo = (relay: string) => { - if (relaysInfo) { - const info = relaysInfo[relay] - - if (info) { - let displayRelaysInfoCopy: string[] = JSON.parse( - JSON.stringify(displayRelaysInfo) - ) - - if (displayRelaysInfoCopy.includes(relay)) { - displayRelaysInfoCopy = displayRelaysInfoCopy.filter( - (rel) => rel !== relay - ) - } else { - displayRelaysInfoCopy.push(relay) - } - - setDisplayRelaysInfo(displayRelaysInfoCopy) - } - } - } - return ( @@ -343,177 +217,211 @@ export const RelaysPage = () => { {relayMap && ( - {Object.keys(relayMap).map((relay, i) => ( - - - - - {relaysInfo && - relaysInfo[relay] && - relaysInfo[relay].limitation && - relaysInfo[relay].limitation?.payment_required && ( - - handleRelayInfo(relay)} - /> - - )} - - - - handleLeaveRelay(relay)} - > - - Leave - - - - - handleRelayInfo(relay)} - className={styles.showInfo} - > - Show info{' '} - {displayRelaysInfo.includes(relay) ? ( - - ) : ( - - )} - - ) : ( - '' - ) - } - /> - handleRelayWriteChange(relay, event)} - /> - - {displayRelaysInfo.includes(relay) && ( - <> - - - - {relaysInfo && - relaysInfo[relay] && - Object.keys(relaysInfo[relay]).map((key: string) => { - const infoTitle = capitalizeFirstLetter( - key.replace('_', ' ') - ) - let infoValue = (relaysInfo[relay] as any)[key] - - switch (key) { - case 'pubkey': - infoValue = shorten(hexToNpub(infoValue), 15) - - break - - case 'limitation': - infoValue = ( -
    - {Object.keys(infoValue).map((valueKey) => ( -
  • - - {capitalizeFirstLetter( - valueKey.split('_').join(' ') - )} - : - {' '} - {`${infoValue[valueKey]}`} -
  • - ))} -
- ) - - break - - case 'fees': - infoValue = ( -
    - {Object.keys(infoValue).map((valueKey) => ( -
  • - - {capitalizeFirstLetter( - valueKey.split('_').join(' ') - )} - : - {' '} - {`${infoValue[valueKey].map((fee: RelayFee) => `${fee.amount} ${fee.unit}`)}`} -
  • - ))} -
- ) - break - default: - break - } - - if (Array.isArray(infoValue)) { - infoValue = infoValue.join(', ') - } - - return ( - - - {infoTitle}: - {' '} - {infoValue} - {key === 'pubkey' ? ( - { - navigator.clipboard.writeText( - hexToNpub( - (relaysInfo[relay] as any)[key] - ) - ) - - toast.success('Copied to clipboard', { - autoClose: 1000, - hideProgressBar: true - }) - }} - /> - ) : null} - - ) - })} -
-
- - )} -
-
+ {Object.keys(relayMap).map((relay) => ( + ))}
)}
) } + +type RelayItemProp = { + relayURI: string + isWriteRelay: boolean + relayInfo?: RelayInfo + handleLeaveRelay: (relay: string) => void + handleRelayWriteChange: ( + relay: string, + event: React.ChangeEvent + ) => Promise +} + +const RelayItem = ({ + relayURI, + isWriteRelay, + relayInfo, + handleLeaveRelay, + handleRelayWriteChange +}: RelayItemProp) => { + const [relayConnectionStatus, setRelayConnectionStatus] = + useState() + + const [displayRelayInfo, setDisplayRelayInfo] = useState(false) + + useDidMount(() => { + relayController.connectRelay(relayURI).then((relay) => { + if (relay && relay.connected) { + setRelayConnectionStatus(RelayConnectionState.Connected) + } else { + setRelayConnectionStatus(RelayConnectionState.NotConnected) + } + }) + }) + + return ( + + + + + {relayInfo && + relayInfo.limitation && + relayInfo.limitation?.payment_required && ( + + setDisplayRelayInfo((prev) => !prev)} + /> + + )} + + + + handleLeaveRelay(relayURI)} + > + + Leave + + + + + setDisplayRelayInfo((prev) => !prev)} + className={styles.showInfo} + > + Show info{' '} + {displayRelayInfo ? ( + + ) : ( + + )} + + ) : ( + '' + ) + } + /> + handleRelayWriteChange(relayURI, event)} + /> + + {displayRelayInfo && ( + <> + + + + {relayInfo && + Object.keys(relayInfo).map((key: string) => { + const infoTitle = capitalizeFirstLetter( + key.replace('_', ' ') + ) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let infoValue = (relayInfo as any)[key] + + switch (key) { + case 'pubkey': + infoValue = shorten(hexToNpub(infoValue), 15) + + break + + case 'limitation': + infoValue = ( +
    + {Object.keys(infoValue).map((valueKey) => ( +
  • + + {capitalizeFirstLetter( + valueKey.split('_').join(' ') + )} + : + {' '} + {`${infoValue[valueKey]}`} +
  • + ))} +
+ ) + + break + + case 'fees': + infoValue = ( +
    + {Object.keys(infoValue).map((valueKey) => ( +
  • + + {capitalizeFirstLetter( + valueKey.split('_').join(' ') + )} + : + {' '} + {`${infoValue[valueKey].map((fee: RelayFee) => `${fee.amount} ${fee.unit}`)}`} +
  • + ))} +
+ ) + break + default: + break + } + + if (Array.isArray(infoValue)) { + infoValue = infoValue.join(', ') + } + + return ( + + + {infoTitle}: + {' '} + {infoValue} + {key === 'pubkey' ? ( + { + navigator.clipboard.writeText( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + hexToNpub((relayInfo as any)[key]) + ) + + toast.success('Copied to clipboard', { + autoClose: 1000, + hideProgressBar: true + }) + }} + /> + ) : null} + + ) + })} +
+
+ + )} +
+
+ ) +} diff --git a/src/utils/dvm.ts b/src/utils/dvm.ts index ba985ed..8995ae7 100644 --- a/src/utils/dvm.ts +++ b/src/utils/dvm.ts @@ -1,12 +1,14 @@ import { EventTemplate, Filter, kinds, nip19 } from 'nostr-tools' -import { queryNip05, unixNow } from '.' +import { compareObjects, queryNip05, unixNow } from '.' import { MetadataController, NostrController, relayController } from '../controllers' -import { NostrJoiningBlock } from '../types' +import { NostrJoiningBlock, RelayInfoObject } from '../types' import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk' +import store from '../store/store' +import { setRelayInfoAction } from '../store/actions' export const getNostrJoiningBlockNumber = async ( hexKey: string @@ -133,3 +135,94 @@ export const getNostrJoiningBlockNumber = async ( return null } + +/** + * Sets information about relays into relays.info app state. + * @param relayURIs - relay URIs to get information about + */ +export const getRelayInfo = async (relayURIs: string[]) => { + // initialize job request + const jobEventTemplate: EventTemplate = { + content: '', + created_at: unixNow(), + kind: 68001, + tags: [ + ['i', `${JSON.stringify(relayURIs)}`], + ['j', 'relay-info'] + ] + } + + const nostrController = NostrController.getInstance() + + // sign job request event + const jobSignedEvent = await nostrController.signEvent(jobEventTemplate) + + const relays = [ + 'wss://relay.damus.io', + 'wss://relay.primal.net', + 'wss://relayable.org' + ] + + // publish job request + await relayController.publish(jobSignedEvent, relays) + + console.log('jobSignedEvent :>> ', jobSignedEvent) + + const subscribeWithTimeout = ( + subscription: NDKSubscription, + timeoutMs: number + ): Promise => { + return new Promise((resolve, reject) => { + const eventHandler = (event: NDKEvent) => { + subscription.stop() + resolve(event.content) + } + + subscription.on('event', eventHandler) + + // Set up a timeout to stop the subscription after a specified time + const timeout = setTimeout(() => { + subscription.stop() // Stop the subscription + reject(new Error('Subscription timed out')) // Reject the promise with a timeout error + }, timeoutMs) + + // Handle subscription close event + subscription.on('close', () => clearTimeout(timeout)) + }) + } + + const dvmNDK = new NDK({ + explicitRelayUrls: relays + }) + + await dvmNDK.connect(2000) + + // filter for getting DVM job's result + const sub = dvmNDK.subscribe({ + kinds: [68002 as number], + '#e': [jobSignedEvent.id], + '#p': [jobSignedEvent.pubkey] + }) + + // asynchronously get block number from dvm job with 20 seconds timeout + const dvmJobResult = await subscribeWithTimeout(sub, 20000) + + if (!dvmJobResult) { + return Promise.reject(`Relay(s) information wasn't received`) + } + + let relaysInfo: RelayInfoObject + + try { + relaysInfo = JSON.parse(dvmJobResult) + } catch (error) { + return Promise.reject(`Invalid relay(s) information.`) + } + + if ( + relaysInfo && + !compareObjects(store.getState().relays?.info, relaysInfo) + ) { + store.dispatch(setRelayInfoAction(relaysInfo)) + } +} diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index 48ce930..f3d6736 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -603,7 +603,7 @@ export const updateUsersAppData = async (meta: Meta) => { const publishResult = await Promise.race([ relayController.publish(signedEvent, writeRelays), - timeout(1000 * 30) + timeout(40 * 1000) ]).catch((err) => { console.log('err :>> ', err) if (err.message === 'Timeout') { @@ -936,7 +936,7 @@ export const sendNotification = async (receiver: string, meta: Meta) => { // Publish the notification event to the recipient's read relays await Promise.race([ relayController.publish(wrappedEvent, relaySet.read), - timeout(1000 * 30) + timeout(40 * 1000) ]).catch((err) => { // Log an error if publishing the notification event fails console.log( diff --git a/src/utils/relays.ts b/src/utils/relays.ts index 2d2d8cd..32bfdd4 100644 --- a/src/utils/relays.ts +++ b/src/utils/relays.ts @@ -1,12 +1,13 @@ import axios from 'axios' -import { Event, Filter } from 'nostr-tools' +import { Event, Filter, kinds, UnsignedEvent } from 'nostr-tools' import { RelayList } from 'nostr-tools/kinds' -import { relayController } from '../controllers/RelayController.ts' +import { getRelayInfo, unixNow } from '.' +import { NostrController, relayController } from '../controllers' import { localCache } from '../services' import { setMostPopularRelaysAction } from '../store/actions' import store from '../store/store' import { RelayMap, RelayReadStats, RelaySet, RelayStats } from '../types' -import { ONE_WEEK_IN_MS, SIGIT_RELAY } from './const.ts' +import { ONE_WEEK_IN_MS, SIGIT_RELAY } from './const' const READ_MARKER = 'read' const WRITE_MARKER = 'write' @@ -154,12 +155,131 @@ const getMostPopularRelays = async ( return apiTopRelays } +/** + * Provides relay map. + * @param npub - user's npub + * @returns - promise that resolves into relay map and a timestamp when it has been updated. + */ +const getRelayMap = async ( + npub: string +): Promise<{ map: RelayMap; mapUpdated?: number }> => { + const mostPopularRelays = await getMostPopularRelays() + + // More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md + const eventFilter: Filter = { + kinds: [kinds.RelayList], + authors: [npub] + } + + const event = await relayController + .fetchEvent(eventFilter, mostPopularRelays) + .catch((err) => { + return Promise.reject(err) + }) + + if (event) { + // Handle founded 10002 event + const relaysMap: RelayMap = {} + + // 'r' stands for 'relay' + const relayTags = event.tags.filter((tag) => tag[0] === 'r') + + relayTags.forEach((tag) => { + const uri = tag[1] + const relayType = tag[2] + + // if 3rd element of relay tag is undefined, relay is WRITE and READ + relaysMap[uri] = { + write: relayType ? relayType === 'write' : true, + read: relayType ? relayType === 'read' : true + } + }) + + Object.keys(relaysMap).forEach((relayUrl) => { + relayController.connectRelay(relayUrl) + }) + + getRelayInfo(Object.keys(relaysMap)) + + return Promise.resolve({ map: relaysMap, mapUpdated: event.created_at }) + } else { + return Promise.resolve({ map: getDefaultRelayMap() }) + } +} + +/** + * Publishes relay map. + * @param relayMap - relay map. + * @param npub - user's npub. + * @param extraRelaysToPublish - optional relays to publish relay map. + * @returns - promise that resolves into a string representing publishing result. + */ +const publishRelayMap = async ( + relayMap: RelayMap, + npub: string, + extraRelaysToPublish?: string[] +): Promise => { + const timestamp = unixNow() + const relayURIs = Object.keys(relayMap) + + // More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md + const tags: string[][] = relayURIs.map((relayURI) => + [ + 'r', + relayURI, + relayMap[relayURI].read && relayMap[relayURI].write + ? '' + : relayMap[relayURI].write + ? 'write' + : 'read' + ].filter((value) => value !== '') + ) + + const newRelayMapEvent: UnsignedEvent = { + kind: kinds.RelayList, + tags, + content: '', + pubkey: npub, + created_at: timestamp + } + + const nostrController = NostrController.getInstance() + const signedEvent = await nostrController.signEvent(newRelayMapEvent) + + let relaysToPublish = relayURIs + + // Add extra relays if provided + if (extraRelaysToPublish) { + relaysToPublish = [...relaysToPublish, ...extraRelaysToPublish] + } + + // If relay map is empty, use most popular relay URIs + if (!relaysToPublish.length) { + relaysToPublish = await getMostPopularRelays() + } + + const publishResult = await relayController.publish( + signedEvent, + relaysToPublish + ) + + if (publishResult && publishResult.length) { + return Promise.resolve( + `Relay Map published on: ${publishResult.join('\n')}` + ) + } + + return Promise.reject('Publishing updated relay map was unsuccessful.') +} + export { findRelayListAndUpdateCache, findRelayListInCache, getDefaultRelayMap, getDefaultRelaySet, getMostPopularRelays, + getRelayMap, + publishRelayMap, getUserRelaySet, isOlderThanOneWeek } From 12765cf758602d2dd8ce37d1427df984fb4049b9 Mon Sep 17 00:00:00 2001 From: daniyal Date: Sun, 18 Aug 2024 22:55:13 +0500 Subject: [PATCH 28/63] chore: remove relay connection status from redux --- src/store/actionTypes.ts | 1 - src/store/relays/action.ts | 10 +--------- src/store/relays/reducer.ts | 9 +-------- src/store/relays/types.ts | 9 +-------- 4 files changed, 3 insertions(+), 26 deletions(-) diff --git a/src/store/actionTypes.ts b/src/store/actionTypes.ts index 7a09430..01ecf99 100644 --- a/src/store/actionTypes.ts +++ b/src/store/actionTypes.ts @@ -16,7 +16,6 @@ export const SET_RELAY_MAP = 'SET_RELAY_MAP' export const SET_RELAY_INFO = 'SET_RELAY_INFO' export const SET_RELAY_MAP_UPDATED = 'SET_RELAY_MAP_UPDATED' export const SET_MOST_POPULAR_RELAYS = 'SET_MOST_POPULAR_RELAYS' -export const SET_RELAY_CONNECTION_STATUS = 'SET_RELAY_CONNECTION_STATUS' export const UPDATE_USER_APP_DATA = 'UPDATE_USER_APP_DATA' export const UPDATE_PROCESSED_GIFT_WRAPS = 'UPDATE_PROCESSED_GIFT_WRAPS' diff --git a/src/store/relays/action.ts b/src/store/relays/action.ts index 6f95840..7552565 100644 --- a/src/store/relays/action.ts +++ b/src/store/relays/action.ts @@ -3,10 +3,9 @@ import { SetRelayMapAction, SetMostPopularRelaysAction, SetRelayInfoAction, - SetRelayConnectionStatusAction, SetRelayMapUpdatedAction } from './types' -import { RelayMap, RelayInfoObject, RelayConnectionStatus } from '../../types' +import { RelayMap, RelayInfoObject } from '../../types' export const setRelayMapAction = (payload: RelayMap): SetRelayMapAction => ({ type: ActionTypes.SET_RELAY_MAP, @@ -27,13 +26,6 @@ export const setMostPopularRelaysAction = ( payload }) -export const setRelayConnectionStatusAction = ( - payload: RelayConnectionStatus -): SetRelayConnectionStatusAction => ({ - type: ActionTypes.SET_RELAY_CONNECTION_STATUS, - payload -}) - export const setRelayMapUpdatedAction = (): SetRelayMapUpdatedAction => ({ type: ActionTypes.SET_RELAY_MAP_UPDATED }) diff --git a/src/store/relays/reducer.ts b/src/store/relays/reducer.ts index b4b9854..f5067bf 100644 --- a/src/store/relays/reducer.ts +++ b/src/store/relays/reducer.ts @@ -5,8 +5,7 @@ const initialState: RelaysState = { map: undefined, mapUpdated: undefined, mostPopular: undefined, - info: undefined, - connectionStatus: undefined + info: undefined } const reducer = ( @@ -26,12 +25,6 @@ const reducer = ( info: { ...state.info, ...action.payload } } - case ActionTypes.SET_RELAY_CONNECTION_STATUS: - return { - ...state, - connectionStatus: action.payload - } - case ActionTypes.SET_MOST_POPULAR_RELAYS: return { ...state, mostPopular: action.payload } diff --git a/src/store/relays/types.ts b/src/store/relays/types.ts index e1c4da8..e90ca3b 100644 --- a/src/store/relays/types.ts +++ b/src/store/relays/types.ts @@ -1,13 +1,12 @@ import * as ActionTypes from '../actionTypes' import { RestoreState } from '../actions' -import { RelayMap, RelayInfoObject, RelayConnectionStatus } from '../../types' +import { RelayMap, RelayInfoObject } from '../../types' export type RelaysState = { map?: RelayMap mapUpdated?: number mostPopular?: string[] info?: RelayInfoObject - connectionStatus?: RelayConnectionStatus } export interface SetRelayMapAction { @@ -25,11 +24,6 @@ export interface SetRelayInfoAction { payload: RelayInfoObject } -export interface SetRelayConnectionStatusAction { - type: typeof ActionTypes.SET_RELAY_CONNECTION_STATUS - payload: RelayConnectionStatus -} - export interface SetRelayMapUpdatedAction { type: typeof ActionTypes.SET_RELAY_MAP_UPDATED } @@ -39,5 +33,4 @@ export type RelaysDispatchTypes = | SetRelayInfoAction | SetRelayMapUpdatedAction | SetMostPopularRelaysAction - | SetRelayConnectionStatusAction | RestoreState From 79f37a842f919f052b08ca2afe341a296e55a18c Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 19 Aug 2024 11:07:19 +0300 Subject: [PATCH 29/63] fix: file path --- src/pages/verify/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 8987191..69f7f0b 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -312,7 +312,7 @@ export const VerifyPage = () => { for (const [fileName, pdf] of Object.entries(files)) { const pages = await addMarks(pdf.file, marksByPage) const blob = await convertToPdfBlob(pages) - zip.file(`/files/${fileName}`, blob) + zip.file(`files/${fileName}`, blob) } const arrayBuffer = await zip From 2f1423a35a2472308a72b7ce6d48762e084caf33 Mon Sep 17 00:00:00 2001 From: daniyal Date: Mon, 19 Aug 2024 16:34:42 +0500 Subject: [PATCH 30/63] chore: added a timeout of 30 seconds to subscription --- src/controllers/RelayController.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/controllers/RelayController.ts b/src/controllers/RelayController.ts index 4ab711c..9bb969c 100644 --- a/src/controllers/RelayController.ts +++ b/src/controllers/RelayController.ts @@ -121,7 +121,7 @@ export class RelayController { return new Promise((resolve) => { if (!relay.connected) { console.log(`${relay.url} : Not connected!`, 'Skipping subscription') - return + return resolve() } // Subscribe to the relay with the specified filter @@ -140,6 +140,14 @@ export class RelayController { resolve() // Resolve the promise when EOSE is received } }) + + // add a 30 sec of timeout to subscription + setTimeout(() => { + if (!sub.closed) { + sub.close() + resolve() + } + }, 30 * 1000) }) }) From 3ae1a74dcd5c790e9f17e76d957d769b957a1c81 Mon Sep 17 00:00:00 2001 From: daniyal Date: Mon, 19 Aug 2024 21:29:15 +0500 Subject: [PATCH 31/63] chore: compare objects before dispatching redux action --- src/pages/settings/relays/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/relays/index.tsx b/src/pages/settings/relays/index.tsx index 73e8c6f..7e86964 100644 --- a/src/pages/settings/relays/index.tsx +++ b/src/pages/settings/relays/index.tsx @@ -19,6 +19,7 @@ import { setRelayMapAction } from '../../../store/actions' import { RelayConnectionState, RelayFee, RelayInfo } from '../../../types' import { capitalizeFirstLetter, + compareObjects, getRelayInfo, getRelayMap, hexToNpub, @@ -43,7 +44,9 @@ export const RelaysPage = () => { useDidMount(() => { if (usersPubkey) { getRelayMap(usersPubkey).then((newRelayMap) => { - dispatch(setRelayMapAction(newRelayMap.map)) + if (!compareObjects(relayMap, newRelayMap.map)) { + dispatch(setRelayMapAction(newRelayMap.map)) + } }) } }) From c274879adc4097a989b0c64e0312a9d8aa7831d6 Mon Sep 17 00:00:00 2001 From: daniyal Date: Tue, 20 Aug 2024 12:53:07 +0500 Subject: [PATCH 32/63] chore: use spread operator in relay reducer for setting mostPopular relays --- src/store/relays/reducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/relays/reducer.ts b/src/store/relays/reducer.ts index f5067bf..68f18a0 100644 --- a/src/store/relays/reducer.ts +++ b/src/store/relays/reducer.ts @@ -26,7 +26,7 @@ const reducer = ( } case ActionTypes.SET_MOST_POPULAR_RELAYS: - return { ...state, mostPopular: action.payload } + return { ...state, mostPopular: [...action.payload] } case ActionTypes.RESTORE_STATE: return action.payload.relays From a7883091266849eca17a959d21289469374ce147 Mon Sep 17 00:00:00 2001 From: daniyal Date: Tue, 20 Aug 2024 13:55:31 +0500 Subject: [PATCH 33/63] chore: update tsdoc for function defination in relay controller --- src/controllers/RelayController.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controllers/RelayController.ts b/src/controllers/RelayController.ts index 9bb969c..12f24c2 100644 --- a/src/controllers/RelayController.ts +++ b/src/controllers/RelayController.ts @@ -78,9 +78,9 @@ export class RelayController { * Asynchronously retrieves multiple event from a set of relays based on a provided filter. * If no relays are specified, it defaults to using connected relays. * - * @param {Filter} filter - The filter criteria to find the event. - * @param {string[]} [relays] - An optional array of relay URLs to search for the event. - * @returns {Promise} - Returns a promise that resolves to the found event or null if not found. + * @param filter - The filter criteria to find the event. + * @param relays - An optional array of relay URLs to search for the event. + * @returns Returns a promise that resolves with an array of events. */ fetchEvents = async ( filter: Filter, @@ -170,9 +170,9 @@ export class RelayController { * Asynchronously retrieves an event from a set of relays based on a provided filter. * If no relays are specified, it defaults to using connected relays. * - * @param {Filter} filter - The filter criteria to find the event. - * @param {string[]} [relays] - An optional array of relay URLs to search for the event. - * @returns {Promise} - Returns a promise that resolves to the found event or null if not found. + * @param filter - The filter criteria to find the event. + * @param relays - An optional array of relay URLs to search for the event. + * @returns Returns a promise that resolves to the found event or null if not found. */ fetchEvent = async ( filter: Filter, From 2c586f3c13f15010b08324557bbd89ba35fd00cb Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 14 Aug 2024 18:59:00 +0200 Subject: [PATCH 34/63] feat(verify-page): add files view and content images --- src/components/UsersDetails.tsx/index.tsx | 8 +- src/layouts/StickySideColumns.module.scss | 8 +- src/pages/verify/index.tsx | 151 ++++++++++++++-------- src/pages/verify/style.module.scss | 24 ++++ src/utils/meta.ts | 10 +- 5 files changed, 142 insertions(+), 59 deletions(-) diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx index fc7d43d..021918c 100644 --- a/src/components/UsersDetails.tsx/index.tsx +++ b/src/components/UsersDetails.tsx/index.tsx @@ -56,7 +56,7 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { typeof usersPubkey !== 'undefined' && signers.includes(hexToNpub(usersPubkey)) - const ext = extractFileExtensions(Object.keys(fileHashes)) + const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes)) return submittedBy ? (
@@ -196,14 +196,14 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { {signedStatus} - {ext.length > 0 ? ( + {extensions.length > 0 ? ( - {ext.length > 1 ? ( + {!isSame ? ( <> Multiple File Types ) : ( - getExtensionIconLabel(ext[0]) + getExtensionIconLabel(extensions[0]) )} ) : ( diff --git a/src/layouts/StickySideColumns.module.scss b/src/layouts/StickySideColumns.module.scss index fd99964..7690822 100644 --- a/src/layouts/StickySideColumns.module.scss +++ b/src/layouts/StickySideColumns.module.scss @@ -23,7 +23,11 @@ grid-gap: 15px; } .content { - max-width: 550px; - width: 550px; + padding: 10px; + border: 10px solid $overlay-background-color; + border-radius: 4px; + + max-width: 590px; + width: 590px; margin: 0 auto; } diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 69f7f0b..017b9c5 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -1,8 +1,8 @@ -import { Box, Button, Tooltip, Typography, useTheme } from '@mui/material' +import { Box, Button, Divider, Tooltip, Typography } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' import { Event, verifyEvent } from 'nostr-tools' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { NostrController } from '../../controllers' @@ -16,10 +16,10 @@ import { parseJson, readContentOfZipEntry, signEventForMetaFile, - shorten + shorten, + getCurrentUserFiles } from '../../utils' import styles from './style.module.scss' -import { Cancel, CheckCircle } from '@mui/icons-material' import { useLocation } from 'react-router-dom' import axios from 'axios' import { PdfFile } from '../../types/drawing.ts' @@ -27,7 +27,8 @@ import { addMarks, convertToPdfBlob, convertToPdfFile, - groupMarksByPage + groupMarksByPage, + inPx } from '../../utils/pdf.ts' import { State } from '../../store/rootReducer.ts' import { useSelector } from 'react-redux' @@ -40,13 +41,77 @@ import { UsersDetails } from '../../components/UsersDetails.tsx/index.tsx' import { UserAvatar } from '../../components/UserAvatar/index.tsx' import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx' import { TooltipChild } from '../../components/TooltipChild.tsx' +import FileList from '../../components/FileList' +import { CurrentUserFile } from '../../types/file.ts' + +interface PdfViewProps { + files: CurrentUserFile[] + currentFile: CurrentUserFile | null +} + +const SlimPdfView = ({ files, currentFile }: PdfViewProps) => { + const pdfRefs = useRef<(HTMLDivElement | null)[]>([]) + useEffect(() => { + if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { + pdfRefs.current[currentFile.id]?.scrollIntoView({ + behavior: 'smooth', + block: 'end' + }) + } + }, [currentFile]) + return ( +
+ {files.map((currentUserFile, i) => { + const { hash, filename, pdfFile, id } = currentUserFile + if (!hash) return + return ( + <> +
(pdfRefs.current[id] = el)} + key={filename} + className={styles.fileWrapper} + > + {pdfFile.pages.map((page, i) => { + return ( +
+ + {page.drawnFields.map((f, i) => ( +
+ ))} +
+ ) + })} +
+ + {i < files.length - 1 && ( + + File Separator + + )} + + ) + })} +
+ ) +} export const VerifyPage = () => { - const theme = useTheme() - const textColor = theme.palette.getContrastText( - theme.palette.background.paper - ) - const location = useLocation() /** * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json @@ -57,6 +122,8 @@ export const VerifyPage = () => { const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } = useSigitMeta(meta) + console.log('----------', meta) + const profiles = useSigitProfiles([ ...(submittedBy ? [submittedBy] : []), ...signers, @@ -72,6 +139,15 @@ export const VerifyPage = () => { [key: string]: string | null }>(fileHashes) const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) + const [currentFile, setCurrentFile] = useState(null) + + useEffect(() => { + if (Object.entries(files).length > 0) { + const tmp = getCurrentUserFiles(files, fileHashes) + + setCurrentFile(tmp[0]) + } + }, [fileHashes, files]) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const nostrController = NostrController.getInstance() @@ -414,51 +490,24 @@ export const VerifyPage = () => { - - {Object.entries(currentFileHashes).map( - ([filename, hash], index) => { - const isValidHash = fileHashes[filename] === hash - - return ( - - - {filename} - - {isValidHash && ( - - - - )} - {!isValidHash && ( - - - - )} - - ) - } - )} - + {currentFile !== null && ( + + )} {displayExportedBy()} - - - } right={} - /> + > + + )} diff --git a/src/pages/verify/style.module.scss b/src/pages/verify/style.module.scss index ea6408f..c368748 100644 --- a/src/pages/verify/style.module.scss +++ b/src/pages/verify/style.module.scss @@ -50,3 +50,27 @@ } } } + +.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; +} diff --git a/src/utils/meta.ts b/src/utils/meta.ts index 4915f19..98ec127 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -142,7 +142,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { ) const files = Object.keys(createSignatureContent.fileHashes) - const extensions = extractFileExtensions(files) + const { extensions } = extractFileExtensions(files) const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] const isCompletelySigned = createSignatureContent.signers.every((signer) => @@ -169,6 +169,10 @@ 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() @@ -178,5 +182,7 @@ export const extractFileExtensions = (fileNames: string[]) => { return result }, []) - return extensions + const isSame = extensions.every((ext) => ext === extensions[0]) + + return { extensions, isSame } } From f88e2ad6804424755dacfc8c89da4fb8c2b90fcc Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 15 Aug 2024 13:47:23 +0200 Subject: [PATCH 35/63] fix(verify-page): parse and show mark values --- src/hooks/useSigitMeta.tsx | 23 +++++++++--- src/pages/verify/index.tsx | 74 +++++++++++++++++++++++++++----------- src/types/core.ts | 5 +++ 3 files changed, 77 insertions(+), 25 deletions(-) diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index a393824..fea5154 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -1,5 +1,10 @@ import { useEffect, useState } from 'react' -import { CreateSignatureEventContent, Meta, SignedEventContent } from '../types' +import { + CreateSignatureEventContent, + DocSignatureEvent, + Meta, + SignedEventContent +} from '../types' import { Mark } from '../types/mark' import { fromUnixTimestamp, @@ -38,7 +43,9 @@ export interface FlatMeta encryptionKey: string | null // Parsed Document Signatures - parsedSignatureEvents: { [signer: `npub1${string}`]: Event } + parsedSignatureEvents: { + [signer: `npub1${string}`]: DocSignatureEvent + } // Calculated completion time completedAt?: number @@ -74,7 +81,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { const [zipUrl, setZipUrl] = useState('') const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{ - [signer: `npub1${string}`]: Event + [signer: `npub1${string}`]: DocSignatureEvent }>({}) const [completedAt, setCompletedAt] = useState() @@ -141,7 +148,10 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { } // Temp. map to hold events and signers - const parsedSignatureEventsMap = new Map<`npub1${string}`, Event>() + const parsedSignatureEventsMap = new Map< + `npub1${string}`, + DocSignatureEvent + >() const signerStatusMap = new Map<`npub1${string}`, SignStatus>() const getPrevSignerSig = (npub: `npub1${string}`) => { @@ -183,9 +193,12 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { if (isValidSignature) { // get the signature of prev signer from the content of current signers signedEvent const prevSignersSig = getPrevSignerSig(npub) - try { const obj: SignedEventContent = JSON.parse(event.content) + parsedSignatureEventsMap.set(npub, { + ...event, + parsedContent: obj + }) if ( obj.prevSig && prevSignersSig && diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 017b9c5..b5628db 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -6,7 +6,11 @@ import { useEffect, useRef, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { NostrController } from '../../controllers' -import { CreateSignatureEventContent, Meta } from '../../types' +import { + CreateSignatureEventContent, + DocSignatureEvent, + Meta +} from '../../types' import { decryptArrayBuffer, extractMarksFromSignedMeta, @@ -43,13 +47,21 @@ import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx' import { TooltipChild } from '../../components/TooltipChild.tsx' import FileList from '../../components/FileList' import { CurrentUserFile } from '../../types/file.ts' +import { Mark } from '../../types/mark.ts' interface PdfViewProps { files: CurrentUserFile[] currentFile: CurrentUserFile | null + parsedSignatureEvents: { + [signer: `npub1${string}`]: DocSignatureEvent + } } -const SlimPdfView = ({ files, currentFile }: PdfViewProps) => { +const SlimPdfView = ({ + files, + currentFile, + parsedSignatureEvents +}: PdfViewProps) => { const pdfRefs = useRef<(HTMLDivElement | null)[]>([]) useEffect(() => { if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { @@ -63,6 +75,7 @@ const SlimPdfView = ({ files, currentFile }: PdfViewProps) => {
{files.map((currentUserFile, i) => { const { hash, filename, pdfFile, id } = currentUserFile + const signatureEvents = Object.keys(parsedSignatureEvents) if (!hash) return return ( <> @@ -73,22 +86,38 @@ const SlimPdfView = ({ files, currentFile }: PdfViewProps) => { className={styles.fileWrapper} > {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 + ) + if (m) { + marks.push(...m) + } + }) return (
- {page.drawnFields.map((f, i) => ( -
- ))} + {marks.map((m) => { + return ( +
+ {m.value} +
+ ) + })}
) })} @@ -119,10 +148,15 @@ export const VerifyPage = () => { */ const { uploadedZip, meta } = location.state || {} - const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } = - useSigitMeta(meta) - - console.log('----------', meta) + const { + submittedBy, + zipUrl, + encryptionKey, + signers, + viewers, + fileHashes, + parsedSignatureEvents + } = useSigitMeta(meta) const profiles = useSigitProfiles([ ...(submittedBy ? [submittedBy] : []), @@ -279,7 +313,6 @@ export const VerifyPage = () => { } } - console.log('fileHashes :>> ', fileHashes) setCurrentFileHashes(fileHashes) setLoadingSpinnerDesc('Parsing meta.json') @@ -506,6 +539,7 @@ export const VerifyPage = () => { )} diff --git a/src/types/core.ts b/src/types/core.ts index 609837f..8583d4a 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -1,5 +1,6 @@ import { Mark } from './mark' import { Keys } from '../store/auth/types' +import { Event } from 'nostr-tools' export enum UserRole { signer = 'Signer', @@ -44,3 +45,7 @@ export interface UserAppData { keyPair?: Keys // this key pair is used for blossom requests authentication blossomUrls: string[] // array for storing Urls for the files that stores all the sigits and processedGiftWraps on blossom } + +export interface DocSignatureEvent extends Event { + parsedContent?: SignedEventContent +} From 78060fa15fb55c597918042559d544ff4528dc24 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 15 Aug 2024 16:43:10 +0200 Subject: [PATCH 36/63] fix(marks): assign selectedMarkValue to currentValue and mark.value --- src/utils/mark.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/mark.ts b/src/utils/mark.ts index 18cc3e8..f0cd47e 100644 --- a/src/utils/mark.ts +++ b/src/utils/mark.ts @@ -119,7 +119,11 @@ const getUpdatedMark = ( return { ...selectedMark, currentValue: selectedMarkValue, - isCompleted: !!selectedMarkValue + isCompleted: !!selectedMarkValue, + mark: { + ...selectedMark.mark, + value: selectedMarkValue + } } } From 7278485b76c6e4a6319d1f3a7941d4985dc93cad Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 15 Aug 2024 16:44:53 +0200 Subject: [PATCH 37/63] fix(verify-page): export (download) files now includes files The issue was noticed on the windows machine, removing forward slash made it work --- src/utils/file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/file.ts b/src/utils/file.ts index 94308d5..619ecff 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -15,7 +15,7 @@ const getZipWithFiles = async ( for (const [fileName, pdf] of Object.entries(files)) { const pages = await addMarks(pdf.file, marksByPage) const blob = await convertToPdfBlob(pages) - zip.file(`/files/${fileName}`, blob) + zip.file(`files/${fileName}`, blob) } return zip From 268a4db3ff211566af3e8cf77838c54d3e9c861e Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 15 Aug 2024 17:02:30 +0200 Subject: [PATCH 38/63] revert: "feat(pdf-marking): adds file validity check" Refs: ed7acd6cb4c73ee2907fb5062a10dbb8d369f7c9 --- src/components/FileList/index.tsx | 12 ++---------- src/components/FileList/style.module.scss | 23 ----------------------- src/pages/sign/index.tsx | 2 +- src/types/file.ts | 1 - src/utils/utils.ts | 7 ++----- 5 files changed, 5 insertions(+), 40 deletions(-) diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx index 7cd30eb..f1fdcd8 100644 --- a/src/components/FileList/index.tsx +++ b/src/components/FileList/index.tsx @@ -1,8 +1,6 @@ import { CurrentUserFile } from '../../types/file.ts' import styles from './style.module.scss' import { Button } from '@mui/material' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faCheck } from '@fortawesome/free-solid-svg-icons' interface FileListProps { files: CurrentUserFile[] @@ -28,14 +26,8 @@ const FileList = ({ className={`${styles.fileItem} ${isActive(file) && styles.active}`} onClick={() => setCurrentFile(file)} > -
{file.id}
-
-
{file.filename}
-
- -
- {file.isHashValid && } -
+ {file.id} + {file.filename} ))} diff --git a/src/components/FileList/style.module.scss b/src/components/FileList/style.module.scss index 6f7b64a..40b83a3 100644 --- a/src/components/FileList/style.module.scss +++ b/src/components/FileList/style.module.scss @@ -21,12 +21,6 @@ ul { padding: 0; /* Removes default padding */ } -li { - list-style-type: none; /* Removes the bullets */ - margin: 0; /* Removes any default margin */ - padding: 0; /* Removes any default padding */ -} - .wrap { display: flex; @@ -89,12 +83,6 @@ li { color: white; } -.fileInfo { - flex-grow: 1; - font-size: 16px; - font-weight: 500; -} - .fileName { display: -webkit-box; -webkit-box-orient: vertical; @@ -109,15 +97,4 @@ li { flex-direction: column; justify-content: center; align-items: center; - width: 10px; -} - -.fileVisual { - display: flex; - flex-shrink: 0; - flex-direction: column; - justify-content: center; - align-items: center; - height: 25px; - width: 25px; } \ No newline at end of file diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index a762292..9f2ab7c 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -952,7 +952,7 @@ export const SignPage = () => { return ( { * including its name, hash, and content * @param files * @param fileHashes - * @param creatorFileHashes */ export const getCurrentUserFiles = ( files: { [filename: string]: PdfFile }, - fileHashes: { [key: string]: string | null }, - creatorFileHashes: { [key: string]: string } + fileHashes: { [key: string]: string | null } ): CurrentUserFile[] => { return Object.entries(files).map(([filename, pdfFile], index) => { return { pdfFile, filename, id: index + 1, - ...(!!fileHashes[filename] && { hash: fileHashes[filename]! }), - isHashValid: creatorFileHashes[filename] === fileHashes[filename] + ...(!!fileHashes[filename] && { hash: fileHashes[filename]! }) } }) } From 423b6b6792feca3ff5891bfc1d9f2181d8a00195 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 15 Aug 2024 17:34:11 +0200 Subject: [PATCH 39/63] fix(verify-page): add mark styling --- src/pages/verify/index.tsx | 3 +-- src/pages/verify/style.module.scss | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index b5628db..aa79b73 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -104,10 +104,9 @@ const SlimPdfView = ({ {marks.map((m) => { return (
Date: Thu, 15 Aug 2024 17:48:05 +0200 Subject: [PATCH 40/63] fix(sigit): add to submittedBy avatar badge for verified sigit creation --- src/components/DisplaySigit/index.tsx | 13 +++++++++---- src/components/UsersDetails.tsx/index.tsx | 11 ++++++++--- src/utils/meta.ts | 8 ++++++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index 0d7407f..92dc01d 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -1,5 +1,5 @@ import { Meta } from '../../types' -import { SigitCardDisplayInfo, SigitStatus } from '../../utils' +import { SigitCardDisplayInfo, SigitStatus, SignStatus } from '../../utils' import { Link } from 'react-router-dom' import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils' import { appPublicRoutes, appPrivateRoutes } from '../../routes' @@ -13,7 +13,6 @@ import { faFile } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { UserAvatar } from '../UserAvatar' import { UserAvatarGroup } from '../UserAvatarGroup' import styles from './style.module.scss' @@ -34,7 +33,8 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { submittedBy, signers, signedStatus, - fileExtensions + fileExtensions, + isValid } = parsedMeta const { signersStatus } = useSigitMeta(meta) @@ -62,6 +62,7 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { const profile = profiles[submittedBy] return ( { disableInteractive > - + ) diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx index 021918c..3681cfd 100644 --- a/src/components/UsersDetails.tsx/index.tsx +++ b/src/components/UsersDetails.tsx/index.tsx @@ -9,7 +9,6 @@ import { shorten, SignStatus } from '../../utils' -import { UserAvatar } from '../UserAvatar' import { useSigitMeta } from '../../hooks/useSigitMeta' import { UserAvatarGroup } from '../UserAvatarGroup' @@ -44,7 +43,8 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { createdAt, completedAt, parsedSignatureEvents, - signedStatus + signedStatus, + isValid } = useSigitMeta(meta) const { usersPubkey } = useSelector((state: State) => state.auth) const profiles = useSigitProfiles([ @@ -68,6 +68,7 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { const profile = profiles[submittedBy] return ( { disableInteractive > - + ) diff --git a/src/utils/meta.ts b/src/utils/meta.ts index 98ec127..276f049 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -1,6 +1,6 @@ import { CreateSignatureEventContent, Meta } from '../types' import { fromUnixTimestamp, parseJson } from '.' -import { Event } from 'nostr-tools' +import { Event, verifyEvent } from 'nostr-tools' import { toast } from 'react-toastify' export enum SignStatus { @@ -75,6 +75,7 @@ export interface SigitCardDisplayInfo { signers: `npub1${string}`[] fileExtensions: string[] signedStatus: SigitStatus + isValid: boolean } /** @@ -128,12 +129,15 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { const sigitInfo: SigitCardDisplayInfo = { signers: [], fileExtensions: [], - signedStatus: SigitStatus.Partial + signedStatus: SigitStatus.Partial, + isValid: false } try { const createSignatureEvent = await parseNostrEvent(meta.createSignature) + sigitInfo.isValid = verifyEvent(createSignatureEvent) + // created_at in nostr events are stored in seconds sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at) From b6479db2665ef500d20831dcd941d5ce728b79d3 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 16 Aug 2024 12:01:41 +0200 Subject: [PATCH 41/63] fix(marks): add file grouping for marks, fix read pdf types --- src/pages/verify/index.tsx | 6 ++--- src/utils/file.ts | 6 ++--- src/utils/pdf.ts | 47 ++++++++++++++++++++++++++------------ 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index aa79b73..865310e 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -31,7 +31,7 @@ import { addMarks, convertToPdfBlob, convertToPdfFile, - groupMarksByPage, + groupMarksByFileNamePage, inPx } from '../../utils/pdf.ts' import { State } from '../../store/rootReducer.ts' @@ -415,10 +415,10 @@ export const VerifyPage = () => { zip.file('meta.json', stringifiedMeta) const marks = extractMarksFromSignedMeta(updatedMeta) - const marksByPage = groupMarksByPage(marks) + const marksByPage = groupMarksByFileNamePage(marks) for (const [fileName, pdf] of Object.entries(files)) { - const pages = await addMarks(pdf.file, marksByPage) + const pages = await addMarks(pdf.file, marksByPage[fileName]) const blob = await convertToPdfBlob(pages) zip.file(`files/${fileName}`, blob) } diff --git a/src/utils/file.ts b/src/utils/file.ts index 619ecff..401d3c4 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,6 +1,6 @@ import { Meta } from '../types' import { extractMarksFromSignedMeta } from './mark.ts' -import { addMarks, convertToPdfBlob, groupMarksByPage } from './pdf.ts' +import { addMarks, convertToPdfBlob, groupMarksByFileNamePage } from './pdf.ts' import JSZip from 'jszip' import { PdfFile } from '../types/drawing.ts' @@ -10,10 +10,10 @@ const getZipWithFiles = async ( ): Promise => { const zip = new JSZip() const marks = extractMarksFromSignedMeta(meta) - const marksByPage = groupMarksByPage(marks) + const marksByFileNamePage = groupMarksByFileNamePage(marks) for (const [fileName, pdf] of Object.entries(files)) { - const pages = await addMarks(pdf.file, marksByPage) + const pages = await addMarks(pdf.file, marksByFileNamePage[fileName]) const blob = await convertToPdfBlob(pages) zip.file(`files/${fileName}`, blob) } diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index cef12c2..ce2f132 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -71,14 +71,19 @@ const isPdf = (file: File) => file.type.toLowerCase().includes('pdf') /** * Reads the pdf file binaries */ -const readPdf = (file: File): Promise => { +const readPdf = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader() - reader.onload = (e: any) => { - const data = e.target.result - - resolve(data) + reader.onload = (e) => { + const data = e.target?.result + // Make sure we only resolve for string or ArrayBuffer type + // They are accepted by PDFJS.getDocument function + if (data && typeof data !== 'undefined') { + resolve(data) + } else { + reject(new Error('File is null or undefined')) + } } reader.onerror = (err) => { @@ -94,7 +99,7 @@ const readPdf = (file: File): Promise => { * Converts pdf to the images * @param data pdf file bytes */ -const pdfToImages = async (data: any): Promise => { +const pdfToImages = async (data: string | ArrayBuffer): Promise => { const images: string[] = [] const pdf = await PDFJS.getDocument(data).promise const canvas = document.createElement('canvas') @@ -142,7 +147,8 @@ const addMarks = async ( canvas.width = viewport.width await page.render({ canvasContext: context!, viewport: viewport }).promise - marksPerPage[i]?.forEach((mark) => draw(mark, context!)) + if (marksPerPage && Object.hasOwn(marksPerPage, i)) + marksPerPage[i]?.forEach((mark) => draw(mark, context!)) images.push(canvas.toDataURL()) } @@ -230,11 +236,11 @@ const convertToPdfFile = async ( * @function scaleMark scales remaining marks in line with SCALE * @function byPage groups remaining Marks by their page marks.location.page */ -const groupMarksByPage = (marks: Mark[]) => { +const groupMarksByFileNamePage = (marks: Mark[]) => { return marks .filter(hasValue) .map(scaleMark) - .reduce<{ [key: number]: Mark[] }>(byPage, {}) + .reduce<{ [filename: string]: { [page: number]: Mark[] } }>(byPage, {}) } /** @@ -245,10 +251,21 @@ const groupMarksByPage = (marks: Mark[]) => { * @param obj - accumulator in the reducer callback * @param mark - current value, i.e. Mark being examined */ -const byPage = (obj: { [key: number]: Mark[] }, mark: Mark) => { - const key = mark.location.page - const curGroup = obj[key] ?? [] - return { ...obj, [key]: [...curGroup, mark] } +const byPage = ( + obj: { [filename: string]: { [page: number]: Mark[] } }, + mark: Mark +) => { + const filename = mark.fileName + const pageNumber = mark.location.page + const pages = obj[filename] ?? {} + const marks = pages[pageNumber] ?? [] + return { + ...obj, + [filename]: { + ...pages, + [pageNumber]: [...marks, mark] + } + } } export { @@ -259,5 +276,5 @@ export { convertToPdfFile, addMarks, convertToPdfBlob, - groupMarksByPage -} \ No newline at end of file + groupMarksByFileNamePage +} From eca31cea4f68730ab5d70428902a04514d268764 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 15 Aug 2024 15:35:37 +0300 Subject: [PATCH 42/63] feat(pdf-marking): adds file validity check --- src/components/FileList/index.tsx | 12 ++++++++++-- src/components/FileList/style.module.scss | 23 +++++++++++++++++++++++ src/pages/sign/index.tsx | 2 +- src/types/file.ts | 1 + src/utils/utils.ts | 7 +++++-- 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx index f1fdcd8..7cd30eb 100644 --- a/src/components/FileList/index.tsx +++ b/src/components/FileList/index.tsx @@ -1,6 +1,8 @@ import { CurrentUserFile } from '../../types/file.ts' import styles from './style.module.scss' import { Button } from '@mui/material' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheck } from '@fortawesome/free-solid-svg-icons' interface FileListProps { files: CurrentUserFile[] @@ -26,8 +28,14 @@ const FileList = ({ className={`${styles.fileItem} ${isActive(file) && styles.active}`} onClick={() => setCurrentFile(file)} > - {file.id} - {file.filename} +
{file.id}
+
+
{file.filename}
+
+ +
+ {file.isHashValid && } +
))} diff --git a/src/components/FileList/style.module.scss b/src/components/FileList/style.module.scss index 40b83a3..6f7b64a 100644 --- a/src/components/FileList/style.module.scss +++ b/src/components/FileList/style.module.scss @@ -21,6 +21,12 @@ ul { padding: 0; /* Removes default padding */ } +li { + list-style-type: none; /* Removes the bullets */ + margin: 0; /* Removes any default margin */ + padding: 0; /* Removes any default padding */ +} + .wrap { display: flex; @@ -83,6 +89,12 @@ ul { color: white; } +.fileInfo { + flex-grow: 1; + font-size: 16px; + font-weight: 500; +} + .fileName { display: -webkit-box; -webkit-box-orient: vertical; @@ -97,4 +109,15 @@ ul { flex-direction: column; justify-content: center; align-items: center; + width: 10px; +} + +.fileVisual { + display: flex; + flex-shrink: 0; + flex-direction: column; + justify-content: center; + align-items: center; + height: 25px; + width: 25px; } \ No newline at end of file diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 9f2ab7c..a762292 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -952,7 +952,7 @@ export const SignPage = () => { return ( { * including its name, hash, and content * @param files * @param fileHashes + * @param creatorFileHashes */ export const getCurrentUserFiles = ( files: { [filename: string]: PdfFile }, - fileHashes: { [key: string]: string | null } + fileHashes: { [key: string]: string | null }, + creatorFileHashes: { [key: string]: string } ): CurrentUserFile[] => { return Object.entries(files).map(([filename, pdfFile], index) => { return { pdfFile, filename, id: index + 1, - ...(!!fileHashes[filename] && { hash: fileHashes[filename]! }) + ...(!!fileHashes[filename] && { hash: fileHashes[filename]! }), + isHashValid: creatorFileHashes[filename] === fileHashes[filename] } }) } From 18637bbbc193f970c03c9b19d522fff29390273f Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 20 Aug 2024 11:44:34 +0200 Subject: [PATCH 43/63] fix: update verify to use file signature check --- src/pages/verify/index.tsx | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 865310e..ceba0eb 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -170,17 +170,23 @@ export const VerifyPage = () => { const [currentFileHashes, setCurrentFileHashes] = useState<{ [key: string]: string | null - }>(fileHashes) + }>({}) const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) const [currentFile, setCurrentFile] = useState(null) + const [signatureFileHashes, setSignatureFileHashes] = useState<{ + [key: string]: string + }>(fileHashes) + + useEffect(() => { + setSignatureFileHashes(fileHashes) + }, [fileHashes]) useEffect(() => { if (Object.entries(files).length > 0) { - const tmp = getCurrentUserFiles(files, fileHashes) - + const tmp = getCurrentUserFiles(files, fileHashes, signatureFileHashes) setCurrentFile(tmp[0]) } - }, [fileHashes, files]) + }, [signatureFileHashes, fileHashes, files]) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const nostrController = NostrController.getInstance() @@ -524,7 +530,11 @@ export const VerifyPage = () => { <> {currentFile !== null && ( { > From 86c8cc00fd9a019690c9f700c2496459ea1d3a54 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 15 Aug 2024 18:57:40 +0200 Subject: [PATCH 44/63] feat(create-page): intial layout and page styling Additional linting fixes --- src/components/DrawPDFFields/index.tsx | 157 +++++++-------- .../DrawPDFFields/style.module.scss | 15 +- src/pages/create/index.tsx | 189 ++++++++++-------- src/pages/create/style.module.scss | 10 +- 4 files changed, 188 insertions(+), 183 deletions(-) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index e98187c..1b36cd7 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -1,20 +1,15 @@ import { AccessTime, CalendarMonth, - ExpandMore, Gesture, - PictureAsPdf, Badge, Work, Close } from '@mui/icons-material' import { Box, - Typography, - Accordion, - AccordionDetails, - AccordionSummary, CircularProgress, + Divider, FormControl, InputLabel, MenuItem, @@ -53,7 +48,7 @@ export const DrawPDFFields = (props: Props) => { const [pdfFiles, setPdfFiles] = useState([]) const [parsingPdf, setParsingPdf] = useState(false) - const [showDrawToolBox, setShowDrawToolBox] = useState(false) + const [showDrawToolBox] = useState(true) const [selectedTool, setSelectedTool] = useState() const [toolbox] = useState([ @@ -95,6 +90,16 @@ 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 + */ + const parsePdfPages = async () => { + const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles) + + setPdfFiles(pdfFiles) + } + setParsingPdf(true) parsePdfPages().finally(() => { @@ -105,7 +110,7 @@ export const DrawPDFFields = (props: Props) => { useEffect(() => { if (pdfFiles) props.onDrawFieldsChange(pdfFiles) - }, [pdfFiles]) + }, [pdfFiles, props]) /** * Drawing events @@ -132,12 +137,15 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param page PdfPage where press happened */ - const onMouseDown = (event: any, page: PdfPage) => { + const onMouseDown = ( + event: React.MouseEvent, + page: PdfPage + ) => { // Proceed only if left click if (event.button !== 0) return // Only allow drawing if mouse is not over other drawn element - const isOverPdfImageWrapper = event.target.tagName === 'IMG' + const isOverPdfImageWrapper = event.currentTarget.tagName === 'IMG' if (!selectedTool || !isOverPdfImageWrapper) { return @@ -185,7 +193,10 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param page PdfPage where moving is happening */ - const onMouseMove = (event: any, page: PdfPage) => { + const onMouseMove = ( + event: React.MouseEvent, + page: PdfPage + ) => { if (mouseState.clicked && selectedTool) { const lastElementIndex = page.drawnFields.length - 1 const lastDrawnField = page.drawnFields[lastElementIndex] @@ -216,7 +227,9 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param drawnField Which we are moving */ - const onDrawnFieldMouseDown = (event: any) => { + const onDrawnFieldMouseDown = ( + event: React.MouseEvent + ) => { event.stopPropagation() // Proceed only if left click @@ -239,11 +252,14 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param drawnField which we are moving */ - const onDranwFieldMouseMove = (event: any, drawnField: DrawnField) => { + const onDranwFieldMouseMove = ( + event: React.MouseEvent, + drawnField: DrawnField + ) => { if (mouseState.dragging) { const { mouseX, mouseY, rect } = getMouseCoordinates( event, - event.target.parentNode + event.currentTarget.parentNode as HTMLElement ) const coordsOffset = mouseState.coordsInWrapper @@ -272,7 +288,9 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param drawnField which we are resizing */ - const onResizeHandleMouseDown = (event: any) => { + const onResizeHandleMouseDown = ( + event: React.MouseEvent + ) => { // Proceed only if left click if (event.button !== 0) return @@ -288,11 +306,14 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param drawnField which we are resizing */ - const onResizeHandleMouseMove = (event: any, drawnField: DrawnField) => { + const onResizeHandleMouseMove = ( + event: React.MouseEvent, + drawnField: DrawnField + ) => { if (mouseState.resizing) { const { mouseX, mouseY } = getMouseCoordinates( event, - event.target.parentNode.parentNode + event.currentTarget.parentNode as HTMLElement ) const width = mouseX - drawnField.left @@ -313,7 +334,7 @@ export const DrawPDFFields = (props: Props) => { * @param drawnFileIndex drawn file index */ const onRemoveHandleMouseDown = ( - event: any, + event: React.MouseEvent, pdfFileIndex: number, pdfPageIndex: number, drawnFileIndex: number @@ -331,7 +352,9 @@ export const DrawPDFFields = (props: Props) => { * so select can work properly * @param event Mouse event */ - const onUserSelectHandleMouseDown = (event: any) => { + const onUserSelectHandleMouseDown = ( + event: React.MouseEvent + ) => { event.stopPropagation() } @@ -341,8 +364,11 @@ export const DrawPDFFields = (props: Props) => { * @param customTarget mouse coordinates relative to this element, if not provided * event.target will be used */ - const getMouseCoordinates = (event: any, customTarget?: any) => { - const target = customTarget ? customTarget : event.target + const getMouseCoordinates = ( + event: React.MouseEvent, + customTarget?: HTMLElement + ) => { + 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. @@ -354,31 +380,6 @@ export const DrawPDFFields = (props: Props) => { } } - /** - * Reads the pdf binary files and converts it's pages to images - * creates the pdfFiles object and sets to a state - */ - const parsePdfPages = async () => { - const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles) - - setPdfFiles(pdfFiles) - } - - /** - * - * @returns if expanded pdf accordion is present - */ - const hasExpandedPdf = () => { - return !!pdfFiles.filter((pdfFile) => !!pdfFile.expanded).length - } - - const handleAccordionExpandChange = (expanded: boolean, pdfFile: PdfFile) => { - pdfFile.expanded = expanded - - refreshPdfFiles() - setShowDrawToolBox(hasExpandedPdf()) - } - /** * Changes the drawing tool * @param drawTool to draw with @@ -398,19 +399,11 @@ export const DrawPDFFields = (props: Props) => { */ const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => { return ( - + <> {pdfFile.pages.map((page, pdfPageIndex: number) => { return (
{ onMouseMove(event, page) @@ -516,7 +509,7 @@ export const DrawPDFFields = (props: Props) => {
) })} -
+ ) } @@ -533,34 +526,26 @@ export const DrawPDFFields = (props: Props) => { } return ( - - - Draw fields on the PDFs: - - {pdfFiles.map((pdfFile, pdfFileIndex: number) => { - return ( - { - handleAccordionExpandChange(expanded, pdfFile) - }} - > - } - aria-controls={`panel${pdfFileIndex}-content`} - id={`panel${pdfFileIndex}header`} +
+ {pdfFiles.map((pdfFile, pdfFileIndex: number) => { + return ( + <> +
+ {getPdfPages(pdfFile, pdfFileIndex)} +
+ {pdfFileIndex < pdfFiles.length - 1 && ( + - - {pdfFile.file.name} - - - {getPdfPages(pdfFile, pdfFileIndex)} - - - ) - })} - + File Separator + + )} + + ) + })} {showDrawToolBox && ( @@ -569,7 +554,7 @@ export const DrawPDFFields = (props: Props) => { .filter((drawTool) => drawTool.active) .map((drawTool: DrawTool, index: number) => { return ( - { handleToolSelect(drawTool) @@ -578,12 +563,12 @@ export const DrawPDFFields = (props: Props) => { > {drawTool.icon} {drawTool.label} - +
) })}
)} - +
) } diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 7490d1f..0d84c4b 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -51,7 +51,6 @@ position: relative; -webkit-user-select: none; user-select: none; - margin-bottom: 10px; > img { display: block; @@ -81,7 +80,7 @@ } &.edited { - border: 1px dotted #01aaad + border: 1px dotted #01aaad; } .resizeHandle { @@ -124,3 +123,15 @@ padding: 5px 0; } } + +.fileWrapper { + display: flex; + flex-direction: column; + gap: 15px; +} + +.view { + display: flex; + flex-direction: column; + gap: 25px; +} diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index fe4ac7f..36e6584 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -64,6 +64,7 @@ import styles from './style.module.scss' import { PdfFile } from '../../types/drawing' import { DrawPDFFields } from '../../components/DrawPDFFields' import { Mark } from '../../types/mark.ts' +import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' export const CreatePage = () => { const navigate = useNavigate() @@ -702,94 +703,110 @@ export const CreatePage = () => { <> {isLoading && } - setTitle(e.target.value)} - variant="outlined" - /> + + setTitle(e.target.value)} + variant="outlined" + /> - - handleSelectFiles(value)} + + handleSelectFiles(value)} + /> + + {selectedFiles.length > 0 && ( +
    + {selectedFiles.map((file, index) => ( +
  • + {file.name} + handleRemoveFile(file)}> + {' '} + +
  • + ))} +
+ )} +
+ + } + right={ + <> + + Add Counterparts + + + + setUserInput(e.target.value)} + helperText={error} + error={!!error} + /> + + Role + + + + + + + + + + + + + + + } + > + - - {selectedFiles.length > 0 && ( -
    - {selectedFiles.map((file, index) => ( -
  • - {file.name} - handleRemoveFile(file)}> - {' '} - -
  • - ))} -
- )} -
- - - Add Counterparts - - - - setUserInput(e.target.value)} - helperText={error} - error={!!error} - /> - - Role - - - - - - - - - - - - - - - - +
) diff --git a/src/pages/create/style.module.scss b/src/pages/create/style.module.scss index 395936b..9304618 100644 --- a/src/pages/create/style.module.scss +++ b/src/pages/create/style.module.scss @@ -1,14 +1,6 @@ @import '../../styles/colors.scss'; .container { - display: flex; - flex-direction: column; - color: $text-color; - margin-top: 10px; - gap: 10px; - width: 550px; - max-width: 550px; - .inputBlock { display: flex; flex-direction: column; @@ -38,4 +30,4 @@ cursor: pointer; } } -} \ No newline at end of file +} From 4cf578b51438711bdb7112c386b268406b7c94ed Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 15 Aug 2024 18:59:06 +0200 Subject: [PATCH 45/63] chore(lint): reduce max warnings --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 447fdda..88a1439 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 25", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 14", "lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint:staged": "eslint --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "formatter:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"", From 6641cf2ee703c4c973c69ecde864030fb2e91596 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 15 Aug 2024 19:50:44 +0200 Subject: [PATCH 46/63] fix: simplify events, more ts and clean up --- src/components/DrawPDFFields/index.tsx | 82 ++++++++++--------- .../DrawPDFFields/style.module.scss | 7 ++ 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 1b36cd7..6bf9ed7 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -48,7 +48,6 @@ export const DrawPDFFields = (props: Props) => { const [pdfFiles, setPdfFiles] = useState([]) const [parsingPdf, setParsingPdf] = useState(false) - const [showDrawToolBox] = useState(true) const [selectedTool, setSelectedTool] = useState() const [toolbox] = useState([ @@ -144,10 +143,7 @@ export const DrawPDFFields = (props: Props) => { // Proceed only if left click if (event.button !== 0) return - // Only allow drawing if mouse is not over other drawn element - const isOverPdfImageWrapper = event.currentTarget.tagName === 'IMG' - - if (!selectedTool || !isOverPdfImageWrapper) { + if (!selectedTool) { return } @@ -199,8 +195,14 @@ export const DrawPDFFields = (props: Props) => { ) => { if (mouseState.clicked && selectedTool) { const lastElementIndex = page.drawnFields.length - 1 + const lastDrawnField = page.drawnFields[lastElementIndex] + // Return early if we don't have lastDrawnField + // Issue noticed in the console when dragging out of bounds + // to the page below (without releaseing mouse click) + if (!lastDrawnField) return + const { mouseX, mouseY } = getMouseCoordinates(event) const width = mouseX - lastDrawnField.left @@ -252,14 +254,14 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param drawnField which we are moving */ - const onDranwFieldMouseMove = ( + const onDrawnFieldMouseMove = ( event: React.MouseEvent, drawnField: DrawnField ) => { if (mouseState.dragging) { const { mouseX, mouseY, rect } = getMouseCoordinates( event, - event.currentTarget.parentNode as HTMLElement + event.currentTarget.parentElement ) const coordsOffset = mouseState.coordsInWrapper @@ -313,7 +315,10 @@ export const DrawPDFFields = (props: Props) => { if (mouseState.resizing) { const { mouseX, mouseY } = getMouseCoordinates( event, - event.currentTarget.parentNode as HTMLElement + // currentTarget = span handle + // 1st parent = drawnField + // 2nd parent = img + event.currentTarget.parentElement?.parentElement ) const width = mouseX - drawnField.left @@ -366,7 +371,7 @@ export const DrawPDFFields = (props: Props) => { */ const getMouseCoordinates = ( event: React.MouseEvent, - customTarget?: HTMLElement + customTarget?: HTMLElement | null ) => { const target = customTarget ? customTarget : event.currentTarget const rect = target.getBoundingClientRect() @@ -405,16 +410,15 @@ export const DrawPDFFields = (props: Props) => {
{ - onMouseMove(event, page) - }} - onMouseDown={(event) => { - onMouseDown(event, page) - }} > { + onMouseMove(event, page) + }} + onMouseDown={(event) => { + onMouseDown(event, page) + }} draggable="false" - style={{ width: '100%' }} src={page.image} /> @@ -424,7 +428,7 @@ export const DrawPDFFields = (props: Props) => { key={drawnFieldIndex} onMouseDown={onDrawnFieldMouseDown} onMouseMove={(event) => { - onDranwFieldMouseMove(event, drawnField) + onDrawnFieldMouseMove(event, drawnField) }} className={styles.drawingRectangle} style={{ @@ -529,7 +533,7 @@ export const DrawPDFFields = (props: Props) => {
{pdfFiles.map((pdfFile, pdfFileIndex: number) => { return ( - <> +
{getPdfPages(pdfFile, pdfFileIndex)}
@@ -543,32 +547,30 @@ export const DrawPDFFields = (props: Props) => { File Separator )} - +
) })} - {showDrawToolBox && ( - - - {toolbox - .filter((drawTool) => drawTool.active) - .map((drawTool: DrawTool, index: number) => { - return ( -
{ - handleToolSelect(drawTool) - }} - className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''}`} - > - {drawTool.icon} - {drawTool.label} -
- ) - })} -
+ + + {toolbox + .filter((drawTool) => drawTool.active) + .map((drawTool: DrawTool, index: number) => { + return ( +
{ + handleToolSelect(drawTool) + }} + className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''}`} + > + {drawTool.icon} + {drawTool.label} +
+ ) + })}
- )} +
) } diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 0d84c4b..249b66c 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -93,6 +93,13 @@ border: 1px solid rgb(160, 160, 160); border-radius: 50%; cursor: pointer; + + // Increase the area a bit so it's easier to click + &::after { + content: ''; + position: absolute; + inset: -14px; + } } .removeHandle { From c2199b79bdd4e352dda618502536a5e16cb81258 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 16 Aug 2024 11:08:03 +0200 Subject: [PATCH 47/63] refactor(create-page): styling --- src/pages/create/index.tsx | 143 +++++++++++++---------------- src/pages/create/style.module.scss | 19 ++-- 2 files changed, 78 insertions(+), 84 deletions(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 36e6584..4d292b2 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -6,7 +6,6 @@ import { IconButton, InputLabel, MenuItem, - Paper, Select, Table, TableBody, @@ -705,99 +704,87 @@ export const CreatePage = () => { +
setTitle(e.target.value)} variant="outlined" /> - - - handleSelectFiles(value)} - /> - - {selectedFiles.length > 0 && ( -
    - {selectedFiles.map((file, index) => ( -
  • - {file.name} - handleRemoveFile(file)}> - {' '} - -
  • - ))} -
- )} -
- +
    + {selectedFiles.length > 0 && + selectedFiles.map((file, index) => ( +
  • + {file.name} + handleRemoveFile(file)}> + {' '} + +
  • + ))} +
+ handleSelectFiles(value)} + /> +
} right={ - <> +
Add Counterparts - - - setUserInput(e.target.value)} - helperText={error} - error={!!error} - /> - - Role - - - - + setUserInput(e.target.value)} + helperText={error} + error={!!error} + /> + + Role + + + + - - +
+ + - - +
+
} > { return ( - + diff --git a/src/pages/create/style.module.scss b/src/pages/create/style.module.scss index 9304618..527ef5c 100644 --- a/src/pages/create/style.module.scss +++ b/src/pages/create/style.module.scss @@ -1,11 +1,18 @@ @import '../../styles/colors.scss'; -.container { - .inputBlock { - display: flex; - flex-direction: column; - gap: 25px; - } +.flexWrap { + display: flex; + flex-direction: column; + gap: 15px; +} + +.paperGroup { + border-radius: 4px; + background: white; + padding: 15px; + display: flex; + flex-direction: column; + gap: 15px; } .subHeader { From 29e6c851504f884cdeae738bb70323a7703f2f74 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 16 Aug 2024 16:14:35 +0200 Subject: [PATCH 48/63] fix(create-page): only show signers in counterpart select --- src/components/DrawPDFFields/index.tsx | 54 +++++++++++++------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 6bf9ed7..99d8b78 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -19,7 +19,7 @@ import styles from './style.module.scss' import { useEffect, useState } from 'react' import * as PDFJS from 'pdfjs-dist' -import { ProfileMetadata, User } from '../../types' +import { ProfileMetadata, User, UserRole } from '../../types' import { PdfFile, DrawTool, @@ -474,36 +474,38 @@ export const DrawPDFFields = (props: Props) => { labelId="counterparts" label="Counterparts" > - {props.users.map((user, index) => { - let displayValue = truncate( - hexToNpub(user.pubkey), - { - length: 16 - } - ) - - const metadata = props.metadata[user.pubkey] - - if (metadata) { - displayValue = truncate( - metadata.name || - metadata.display_name || - metadata.username, + {props.users + .filter((u) => u.role === UserRole.signer) + .map((user, index) => { + let displayValue = truncate( + hexToNpub(user.pubkey), { length: 16 } ) - } - return ( - - {displayValue} - - ) - })} + const metadata = props.metadata[user.pubkey] + + if (metadata) { + displayValue = truncate( + metadata.name || + metadata.display_name || + metadata.username, + { + length: 16 + } + ) + } + + return ( + + {displayValue} + + ) + })} From 48f54d8568f86f205859b2bd9ef5fd3491c8448b Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 19 Aug 2024 08:30:22 +0200 Subject: [PATCH 49/63] refactor(create-page): update designs and add files navigation --- src/components/DrawPDFFields/index.tsx | 11 +- .../DrawPDFFields/style.module.scss | 4 + src/layouts/StickySideColumns.module.scss | 3 + src/pages/create/index.tsx | 427 +++++++++++------- src/pages/create/style.module.scss | 111 ++++- 5 files changed, 361 insertions(+), 195 deletions(-) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 99d8b78..992f273 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -535,12 +535,17 @@ export const DrawPDFFields = (props: Props) => {
{pdfFiles.map((pdfFile, pdfFileIndex: number) => { return ( -
-
+ <> +
{getPdfPages(pdfFile, pdfFileIndex)}
{pdfFileIndex < pdfFiles.length - 1 && ( { File Separator )} -
+ ) })} diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 249b66c..e0a09dc 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -1,3 +1,5 @@ +@import '../../styles/sizes.scss'; + .pdfFieldItem { background: white; padding: 10px; @@ -135,6 +137,8 @@ display: flex; flex-direction: column; gap: 15px; + position: relative; + scroll-margin-top: $header-height + $body-vertical-padding; } .view { diff --git a/src/layouts/StickySideColumns.module.scss b/src/layouts/StickySideColumns.module.scss index 7690822..7495cad 100644 --- a/src/layouts/StickySideColumns.module.scss +++ b/src/layouts/StickySideColumns.module.scss @@ -10,6 +10,9 @@ .sidesWrap { position: relative; + + // HACK: Stop grid column from growing + min-width: 0; } .sides { diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 4d292b2..57e41b0 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -1,26 +1,18 @@ -import { Clear, DragHandle } from '@mui/icons-material' +import { DragHandle } from '@mui/icons-material' import { - Box, Button, - FormControl, IconButton, - InputLabel, + ListItem, + ListItemIcon, + ListItemText, MenuItem, Select, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, TextField, - Tooltip, - Typography + Tooltip } from '@mui/material' import type { Identifier, XYCoord } from 'dnd-core' import saveAs from 'file-saver' import JSZip from 'jszip' -import { MuiFileInput } from 'mui-file-input' import { Event, kinds } from 'nostr-tools' import { useEffect, useRef, useState } from 'react' import { DndProvider, useDrag, useDrop } from 'react-dnd' @@ -64,6 +56,14 @@ import { PdfFile } from '../../types/drawing' import { DrawPDFFields } from '../../components/DrawPDFFields' import { Mark } from '../../types/mark.ts' import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { + faEye, + faPen, + faPlus, + faTrash, + faUpload +} from '@fortawesome/free-solid-svg-icons' export const CreatePage = () => { const navigate = useNavigate() @@ -76,7 +76,14 @@ export const CreatePage = () => { const [authUrl, setAuthUrl] = useState() const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`) + const [selectedFiles, setSelectedFiles] = useState([]) + const fileInputRef = useRef(null) + const handleUploadButtonClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click() + } + } const [userInput, setUserInput] = useState('') const [userRole, setUserRole] = useState(UserRole.signer) @@ -268,19 +275,22 @@ export const CreatePage = () => { }) } - const handleSelectFiles = (files: File[]) => { - setSelectedFiles((prev) => { - const prevFileNames = prev.map((file) => file.name) - - const newFiles = files.filter( - (file) => !prevFileNames.includes(file.name) - ) - - return [...prev, ...newFiles] - }) + const handleSelectFiles = (event: React.ChangeEvent) => { + if (event.target.files) { + setSelectedFiles(Array.from(event.target.files)) + } } - const handleRemoveFile = (fileToRemove: File) => { + const handleFileClick = (name: string) => { + document.getElementById(name)?.scrollIntoView({ behavior: 'smooth' }) + } + + const handleRemoveFile = ( + event: React.MouseEvent, + fileToRemove: File + ) => { + event.stopPropagation() + setSelectedFiles((prevFiles) => prevFiles.filter((file) => file.name !== fileToRemove.name) ) @@ -705,71 +715,134 @@ export const CreatePage = () => { - setTitle(e.target.value)} - variant="outlined" - /> -
    +
    + setTitle(e.target.value)} + sx={{ + width: '100%', + fontSize: '16px', + '& .MuiInputBase-input': { + padding: '7px 14px' + }, + '& .MuiOutlinedInput-notchedOutline': { + display: 'none' + } + }} + /> +
    +
      {selectedFiles.length > 0 && selectedFiles.map((file, index) => ( -
    1. - {file.name} - handleRemoveFile(file)}> - {' '} - -
    2. + + + ))} -
- handleSelectFiles(value)} - /> + +
} right={
- - Add Counterparts - - +
setUserInput(e.target.value)} helperText={error} error={!!error} + fullWidth + sx={{ + fontSize: '16px', + '& .MuiInputBase-input': { + padding: '7px 14px' + }, + '& .MuiOutlinedInput-notchedOutline': { + display: 'none' + } + }} /> - - Role - - - + - +
{ handleRemoveUser={handleRemoveUser} moveSigner={moveSigner} /> - -
+ +
} > @@ -815,80 +888,76 @@ const DisplayUser = ({ moveSigner }: DisplayUsersProps) => { return ( - -
- - - User - Role - Action - - - - - {users - .filter((user) => user.role === UserRole.signer) - .map((user, index) => ( - + + {users + .filter((user) => user.role === UserRole.signer) + .map((user, index) => ( + + ))} + + {users + .filter((user) => user.role === UserRole.viewer) + .map((user, index) => { + const userMeta = metadata[user.pubkey] + return ( +
+
+ - ))} - - {users - .filter((user) => user.role === UserRole.viewer) - .map((user, index) => { - const userMeta = metadata[user.pubkey] - return ( - - - - - - - - - - handleRemoveUser(user.pubkey)}> - - - - - - ) - })} - -
-
+
+ + + handleRemoveUser(user.pubkey)}> + + + +
+ ) + })} + ) } @@ -992,16 +1061,14 @@ const SignerRow = ({ drag(drop(ref)) return ( - - - + +
- - - null} + renderValue={(value) => ( + + )} + onChange={(e) => + handleUserRoleChange(e.target.value as UserRole, user.pubkey) + } + sx={{ + fontSize: '16px', + minWidth: '44px', + '& .MuiInputBase-input': { + padding: '7px 14px!important', + textOverflow: 'unset!important' + }, + '& .MuiOutlinedInput-notchedOutline': { + display: 'none' } - > - {UserRole.signer} - {UserRole.viewer} - - - - - handleRemoveUser(user.pubkey)}> - - - - - + }} + > + {UserRole.signer} + {UserRole.viewer} + + + handleRemoveUser(user.pubkey)}> + + + +
) } diff --git a/src/pages/create/style.module.scss b/src/pages/create/style.module.scss index 527ef5c..f838f67 100644 --- a/src/pages/create/style.module.scss +++ b/src/pages/create/style.module.scss @@ -6,6 +6,60 @@ gap: 15px; } +.orderedFilesList { + counter-reset: item; + list-style-type: none; + margin: 0; + + li { + display: flex; + align-items: center; + + transition: ease 0.4s; + border-radius: 4px; + background: #ffffff; + padding: 7px 10px; + color: rgba(0, 0, 0, 0.5); + min-height: 45px; + cursor: pointer; + gap: 10px; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + &::before { + content: counter(item) ' '; + counter-increment: item; + font-size: 14px; + } + :nth-child(1) { + flex-grow: 1; + + font-size: 16px; + + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + button { + color: $primary-main; + } + + &:hover, + &.active, + &:focus-within { + background: $primary-main; + color: white; + + button { + color: white; + } + } + } +} + .paperGroup { border-radius: 4px; background: white; @@ -13,28 +67,49 @@ display: flex; flex-direction: column; gap: 15px; + + // Automatic scrolling if paper-group gets large enough + // used for files on the left and users on the right + max-height: 350px; + overflow-x: hidden; + overflow-y: auto; } -.subHeader { - border-bottom: 0.5px solid; -} +.inputWrapper { + display: flex; + align-items: center; -.tableHeaderCell { - border-right: 1px solid rgba(224, 224, 224, 1); -} + height: 34px; + overflow: hidden; + border-radius: 4px; + outline: solid 1px #dddddd; + background: white; -.tableCell { - border-right: 1px solid rgba(224, 224, 224, 1); - height: 56px; + width: 100%; - .user { - display: flex; - align-items: center; - gap: 10px; - - .name { - text-align: center; - cursor: pointer; - } + &:focus-within { + outline-color: $primary-main; + } +} + +.user { + display: flex; + gap: 10px; + + font-size: 14px; + text-align: start; + justify-content: center; + align-items: center; + + a:hover { + text-decoration: none; + } +} + +.avatar { + flex-grow: 1; + + &:first-child { + margin-left: 34px; } } From 1caeb48e6c91e0c8fd052caa8df0a5299f3e5f03 Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 19 Aug 2024 14:55:26 +0200 Subject: [PATCH 50/63] fix(create-page): file list --- src/components/DrawPDFFields/index.tsx | 28 ++++++++++++-------------- src/pages/create/index.tsx | 20 +++++++++++------- src/pages/create/style.module.scss | 11 ++++++---- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 992f273..62216e1 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -16,7 +16,7 @@ import { Select } from '@mui/material' import styles from './style.module.scss' -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import * as PDFJS from 'pdfjs-dist' import { ProfileMetadata, User, UserRole } from '../../types' @@ -535,26 +535,24 @@ export const DrawPDFFields = (props: Props) => {
{pdfFiles.map((pdfFile, pdfFileIndex: number) => { return ( - <> +
{getPdfPages(pdfFile, pdfFileIndex)} + {pdfFileIndex < pdfFiles.length - 1 && ( + + File Separator + + )}
- {pdfFileIndex < pdfFiles.length - 1 && ( - - File Separator - - )} - +
) })} diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 57e41b0..970e001 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -52,6 +52,7 @@ import { } from '../../utils' import { Container } from '../../components/Container' import styles from './style.module.scss' +import fileListStyles from '../../components/FileList/style.module.scss' import { PdfFile } from '../../types/drawing' import { DrawPDFFields } from '../../components/DrawPDFFields' import { Mark } from '../../types/mark.ts' @@ -69,6 +70,8 @@ export const CreatePage = () => { const navigate = useNavigate() const location = useLocation() const { uploadedFiles } = location.state || {} + const [currentFile, setCurrentFile] = useState() + const isActive = (file: File) => file.name === currentFile?.name const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -281,8 +284,8 @@ export const CreatePage = () => { } } - const handleFileClick = (name: string) => { - document.getElementById(name)?.scrollIntoView({ behavior: 'smooth' }) + const handleFileClick = (id: string) => { + document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' }) } const handleRemoveFile = ( @@ -737,13 +740,16 @@ export const CreatePage = () => {
    {selectedFiles.length > 0 && selectedFiles.map((file, index) => ( - - +
))} + + {!!error && ( + {error} + )}
} > @@ -934,6 +940,8 @@ const DisplayUser = ({ IconComponent={() => null} renderValue={(value) => ( )} @@ -942,9 +950,12 @@ const DisplayUser = ({ } sx={{ fontSize: '16px', - minWidth: '44px', + minWidth: '34px', + maxWidth: '34px', + minHeight: '34px', + maxHeight: '34px', '& .MuiInputBase-input': { - padding: '7px 14px!important', + padding: '10px !important', textOverflow: 'unset!important' }, '& .MuiOutlinedInput-notchedOutline': { @@ -956,9 +967,20 @@ const DisplayUser = ({ {UserRole.viewer} - handleRemoveUser(user.pubkey)}> - - +
) @@ -1073,7 +1095,7 @@ const SignerRow = ({ data-handler-id={handlerId} ref={ref} > - +
null} renderValue={(value) => ( - + )} onChange={(e) => handleUserRoleChange(e.target.value as UserRole, user.pubkey) } sx={{ fontSize: '16px', - minWidth: '44px', + minWidth: '34px', + maxWidth: '34px', + minHeight: '34px', + maxHeight: '34px', '& .MuiInputBase-input': { - padding: '7px 14px!important', + padding: '10px !important', textOverflow: 'unset!important' }, '& .MuiOutlinedInput-notchedOutline': { @@ -1112,9 +1141,20 @@ const SignerRow = ({ {UserRole.viewer} - handleRemoveUser(user.pubkey)}> - - +
) diff --git a/src/pages/create/style.module.scss b/src/pages/create/style.module.scss index b47e2f3..e85323d 100644 --- a/src/pages/create/style.module.scss +++ b/src/pages/create/style.module.scss @@ -104,9 +104,16 @@ .avatar { flex-grow: 1; + min-width: 0; &:first-child { - margin-left: 34px; + margin-left: 24px; + } + + img { + // Override the default avatar size + width: 30px; + height: 30px; } } From 19f7151db7eb8a1122ec5252f14b934f0f676113 Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 19 Aug 2024 18:05:14 +0200 Subject: [PATCH 52/63] refactor(create-page): toolbox update --- src/components/DrawPDFFields/index.tsx | 84 +------- .../DrawPDFFields/style.module.scss | 42 ---- src/pages/create/index.tsx | 181 +++++++++++++++++- src/pages/create/style.module.scss | 47 +++++ src/types/drawing.ts | 15 +- 5 files changed, 245 insertions(+), 124 deletions(-) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 62216e1..7e87cb3 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -1,11 +1,4 @@ -import { - AccessTime, - CalendarMonth, - Gesture, - Badge, - Work, - Close -} from '@mui/icons-material' +import { Close } from '@mui/icons-material' import { Box, CircularProgress, @@ -22,11 +15,10 @@ import * as PDFJS from 'pdfjs-dist' import { ProfileMetadata, User, UserRole } from '../../types' import { PdfFile, - DrawTool, MouseState, PdfPage, DrawnField, - MarkType + DrawTool } from '../../types/drawing' import { truncate } from 'lodash' import { hexToNpub } from '../../utils' @@ -41,48 +33,15 @@ interface Props { users: User[] metadata: { [key: string]: ProfileMetadata } onDrawFieldsChange: (pdfFiles: PdfFile[]) => void + selectedTool?: DrawTool } export const DrawPDFFields = (props: Props) => { - const { selectedFiles } = props + const { selectedFiles, selectedTool } = props const [pdfFiles, setPdfFiles] = useState([]) const [parsingPdf, setParsingPdf] = useState(false) - const [selectedTool, setSelectedTool] = useState() - const [toolbox] = useState([ - { - identifier: MarkType.SIGNATURE, - icon: , - label: 'Signature', - active: false - }, - { - identifier: MarkType.FULLNAME, - icon: , - label: 'Full Name', - active: true - }, - { - identifier: MarkType.JOBTITLE, - icon: , - label: 'Job Title', - active: false - }, - { - identifier: MarkType.DATE, - icon: , - label: 'Date', - active: false - }, - { - identifier: MarkType.DATETIME, - icon: , - label: 'Datetime', - active: false - } - ]) - const [mouseState, setMouseState] = useState({ clicked: false }) @@ -385,20 +344,6 @@ export const DrawPDFFields = (props: Props) => { } } - /** - * Changes the drawing tool - * @param drawTool to draw with - */ - const handleToolSelect = (drawTool: DrawTool) => { - // If clicked on the same tool, unselect - if (drawTool.identifier === selectedTool?.identifier) { - setSelectedTool(null) - return - } - - setSelectedTool(drawTool) - } - /** * Renders the pdf pages and drawing elements */ @@ -555,27 +500,6 @@ export const DrawPDFFields = (props: Props) => { ) })} - - - - {toolbox - .filter((drawTool) => drawTool.active) - .map((drawTool: DrawTool, index: number) => { - return ( -
{ - handleToolSelect(drawTool) - }} - className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''}`} - > - {drawTool.icon} - {drawTool.label} -
- ) - })} -
-
) } diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index e0a09dc..855325e 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -7,48 +7,6 @@ cursor: pointer; } -.drawToolBoxContainer { - position: fixed; - bottom: 0; - left: 0; - right: 0; - display: flex; - justify-content: center; - z-index: 50; - - .drawToolBox { - display: flex; - gap: 10px; - min-width: 100px; - background-color: white; - padding: 15px; - box-shadow: 0 0 10px 1px #0000003b; - border-radius: 4px; - - .toolItem { - display: flex; - flex-direction: column; - align-items: center; - border: 1px solid rgba(0, 0, 0, 0.137); - padding: 5px; - cursor: pointer; - -webkit-user-select: none; - user-select: none; - - &.selected { - border-color: #01aaad; - color: #01aaad; - } - - &:not(.selected) { - &:hover { - border-color: #01aaad79; - } - } - } - } -} - .pdfImageWrapper { position: relative; -webkit-user-select: none; diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 35780cb..a544d0f 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -51,16 +51,35 @@ import { import { Container } from '../../components/Container' import styles from './style.module.scss' import fileListStyles from '../../components/FileList/style.module.scss' -import { PdfFile } from '../../types/drawing' +import { DrawTool, MarkType, PdfFile } from '../../types/drawing' import { DrawPDFFields } from '../../components/DrawPDFFields' import { Mark } from '../../types/mark.ts' import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { + fa1, + faBriefcase, + faCalendarDays, + faCheckDouble, + faCircleDot, + faClock, + faCreditCard, + faEllipsis, faEye, faGripLines, + faHeading, + faIdCard, + faImage, + faPaperclip, faPen, + faPhone, faPlus, + faSignature, + faSquareCaretDown, + faSquareCheck, + faStamp, + faT, + faTableCellsLarge, faTrash, faUpload } from '@fortawesome/free-solid-svg-icons' @@ -108,6 +127,132 @@ export const CreatePage = () => { ) const [drawnPdfs, setDrawnPdfs] = useState([]) + const [selectedTool, setSelectedTool] = useState() + const [toolbox] = useState([ + { + identifier: MarkType.TEXT, + icon: , + label: 'Text', + active: false + }, + { + identifier: MarkType.SIGNATURE, + icon: , + label: 'Signature', + active: false + }, + { + identifier: MarkType.JOBTITLE, + icon: , + label: 'Job Title', + active: false + }, + { + identifier: MarkType.FULLNAME, + icon: , + label: 'Full Name', + active: true + }, + { + identifier: MarkType.INITIALS, + icon: , + label: 'Initials', + active: false + }, + { + identifier: MarkType.DATETIME, + icon: , + label: 'Date Time', + active: false + }, + { + identifier: MarkType.DATE, + icon: , + label: 'Date', + active: false + }, + { + identifier: MarkType.NUMBER, + icon: , + label: 'Number', + active: false + }, + { + identifier: MarkType.IMAGES, + icon: , + label: 'Images', + active: false + }, + { + identifier: MarkType.CHECKBOX, + icon: , + label: 'Checkbox', + active: false + }, + { + identifier: MarkType.MULTIPLE, + icon: , + label: 'Multiple', + active: false + }, + { + identifier: MarkType.FILE, + icon: , + label: 'File', + active: false + }, + { + identifier: MarkType.RADIO, + icon: , + label: 'Radio', + active: false + }, + { + identifier: MarkType.SELECT, + icon: , + label: 'Select', + active: false + }, + { + identifier: MarkType.CELLS, + icon: , + label: 'Cells', + active: false + }, + { + identifier: MarkType.STAMP, + icon: , + label: 'Stamp', + active: false + }, + { + identifier: MarkType.PAYMENT, + icon: , + label: 'Payment', + active: false + }, + { + identifier: MarkType.PHONE, + icon: , + label: 'Phone', + active: false + } + ]) + + /** + * Changes the drawing tool + * @param drawTool to draw with + */ + const handleToolSelect = (drawTool: DrawTool) => { + // If clicked on the same tool, unselect + if (drawTool.identifier === selectedTool?.identifier) { + setSelectedTool(undefined) + return + } + + setSelectedTool(drawTool) + } + useEffect(() => { users.forEach((user) => { if (!(user.pubkey in metadata)) { @@ -866,6 +1011,39 @@ export const CreatePage = () => { Publish +
+ {toolbox.map((drawTool: DrawTool, index: number) => { + return ( +
{ + handleToolSelect(drawTool) + } + : () => null + } + className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${!drawTool.active ? styles.comingSoon : ''} + `} + > + {drawTool.icon} + {drawTool.label} + {drawTool.active ? ( + + ) : ( + + Coming soon + + )} +
+ ) + })} +
+ {!!error && ( {error} )} @@ -877,6 +1055,7 @@ export const CreatePage = () => { users={users} selectedFiles={selectedFiles} onDrawFieldsChange={onDrawFieldsChange} + selectedTool={selectedTool} /> diff --git a/src/pages/create/style.module.scss b/src/pages/create/style.module.scss index e85323d..1f40fa6 100644 --- a/src/pages/create/style.module.scss +++ b/src/pages/create/style.module.scss @@ -123,3 +123,50 @@ overflow: hidden; flex-grow: 1; } + +.toolbox { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 15px; + + max-height: 450px; + overflow-x: hidden; + overflow-y: auto; +} + +.toolItem { + width: 90px; + height: 90px; + + transition: ease 0.2s; + display: inline-flex; + flex-direction: column; + gap: 5px; + border-radius: 4px; + padding: 10px 5px 5px 5px; + background: rgba(0, 0, 0, 0.05); + color: rgba(0, 0, 0, 0.5); + align-items: center; + justify-content: center; + font-size: 14px; + cursor: pointer; + -webkit-user-select: none; + user-select: none; + + &.selected { + background: $primary-main; + color: white; + } + + &:not(.selected) { + &:hover { + background: $primary-light; + color: white; + } + } + + &.comingSoon { + opacity: 0.5; + cursor: not-allowed; + } +} diff --git a/src/types/drawing.ts b/src/types/drawing.ts index f14dbea..1e65038 100644 --- a/src/types/drawing.ts +++ b/src/types/drawing.ts @@ -41,9 +41,22 @@ export interface DrawTool { } export enum MarkType { + TEXT = 'TEXT', SIGNATURE = 'SIGNATURE', JOBTITLE = 'JOBTITLE', FULLNAME = 'FULLNAME', + INITIALS = 'INITIALS', + DATETIME = 'DATETIME', DATE = 'DATE', - DATETIME = 'DATETIME' + NUMBER = 'NUMBER', + IMAGES = 'IMAGES', + CHECKBOX = 'CHECKBOX', + MULTIPLE = 'MULTIPLE', + FILE = 'FILE', + RADIO = 'RADIO', + SELECT = 'SELECT', + CELLS = 'CELLS', + STAMP = 'STAMP', + PAYMENT = 'PAYMENT', + PHONE = 'PHONE' } From 0d1a7ba17118996d91c57737cfac4b036d796e31 Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 19 Aug 2024 18:16:48 +0200 Subject: [PATCH 53/63] fix(draw): add resize cursor to resize handle --- src/components/DrawPDFFields/style.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 855325e..8c888ec 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -52,7 +52,7 @@ background-color: #fff; border: 1px solid rgb(160, 160, 160); border-radius: 50%; - cursor: pointer; + cursor: nwse-resize; // Increase the area a bit so it's easier to click &::after { From 6f88f22933ddd6bf787c69d0dcaf12032d5ea4f9 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 20 Aug 2024 09:23:42 +0200 Subject: [PATCH 54/63] fix: leaky styling and warnings Closes #147 --- src/components/FileList/style.module.scss | 1 - .../MarkFormField/style.module.scss | 92 +++++++++---------- src/components/UserAvatar/index.tsx | 2 +- src/pages/create/index.tsx | 5 +- src/pages/create/style.module.scss | 4 + src/pages/home/index.tsx | 6 +- 6 files changed, 58 insertions(+), 52 deletions(-) diff --git a/src/components/FileList/style.module.scss b/src/components/FileList/style.module.scss index 9a379ea..22d8515 100644 --- a/src/components/FileList/style.module.scss +++ b/src/components/FileList/style.module.scss @@ -83,7 +83,6 @@ li { } .fileItem:hover { - transition: ease 0.2s; background: #4c82a3; color: white; } diff --git a/src/components/MarkFormField/style.module.scss b/src/components/MarkFormField/style.module.scss index b5c6bb9..ef80df0 100644 --- a/src/components/MarkFormField/style.module.scss +++ b/src/components/MarkFormField/style.module.scss @@ -8,6 +8,39 @@ left: 0; align-items: center; z-index: 1000; + + button { + transition: ease 0.2s; + width: auto; + border-radius: 4px; + outline: unset; + border: unset; + background: unset; + color: #ffffff; + background: #4c82a3; + font-weight: 500; + font-size: 14px; + padding: 8px 15px; + white-space: nowrap; + display: flex; + flex-direction: row; + grid-gap: 12px; + justify-content: center; + align-items: center; + text-decoration: unset; + position: relative; + cursor: pointer; + } + + button:hover { + background: #5e8eab; + color: white; + } + + button:active { + background: #447592; + color: white; + } } .actions { @@ -19,7 +52,7 @@ flex-direction: column; align-items: center; grid-gap: 15px; - box-shadow: 0 -2px 4px 0 rgb(0,0,0,0.1); + box-shadow: 0 -2px 4px 0 rgb(0, 0, 0, 0.1); max-width: 750px; &.expanded { @@ -73,7 +106,7 @@ .textInput { height: 100px; - background: rgba(0,0,0,0.1); + background: rgba(0, 0, 0, 0.1); border-radius: 4px; border: solid 2px #4c82a3; display: flex; @@ -84,17 +117,19 @@ .input { border-radius: 4px; - border: solid 1px rgba(0,0,0,0.15); + border: solid 1px rgba(0, 0, 0, 0.15); padding: 5px 10px; font-size: 16px; width: 100%; - background: linear-gradient(rgba(0,0,0,0.00), rgba(0,0,0,0.00) 100%), linear-gradient(white, white); + background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0) 100%), + linear-gradient(white, white); } .input:focus { - border: solid 1px rgba(0,0,0,0.15); + border: solid 1px rgba(0, 0, 0, 0.15); outline: none; - background: linear-gradient(rgba(0,0,0,0.05), rgba(0,0,0,0.05) 100%), linear-gradient(white, white); + background: linear-gradient(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.05) 100%), + linear-gradient(white, white); } .actionsBottom { @@ -105,41 +140,6 @@ align-items: center; } -button { - transition: ease 0.2s; - width: auto; - border-radius: 4px; - outline: unset; - border: unset; - background: unset; - color: #ffffff; - background: #4c82a3; - font-weight: 500; - font-size: 14px; - padding: 8px 15px; - white-space: nowrap; - display: flex; - flex-direction: row; - grid-gap: 12px; - justify-content: center; - align-items: center; - text-decoration: unset; - position: relative; - cursor: pointer; -} - -button:hover { - transition: ease 0.2s; - background: #5e8eab; - color: white; -} - -button:active { - transition: ease 0.2s; - background: #447592; - color: white; -} - .submitButton { width: 100%; max-width: 300px; @@ -172,18 +172,18 @@ button:active { font-size: 12px; padding: 5px 10px; border-radius: 3px; - background: rgba(0,0,0,0.1); - color: rgba(0,0,0,0.5); + background: rgba(0, 0, 0, 0.1); + color: rgba(0, 0, 0, 0.5); } .paginationButton:hover { background: #447592; - color: rgba(255,255,255,0.5); + color: rgba(255, 255, 255, 0.5); } .paginationButtonDone { background: #5e8eab; - color: rgb(255,255,255); + color: rgb(255, 255, 255); } .paginationButtonCurrent { @@ -204,7 +204,7 @@ button:active { background: white; color: #434343; padding: 5px 30px; - box-shadow: 0px -3px 4px 0 rgb(0,0,0,0.1); + box-shadow: 0px -3px 4px 0 rgb(0, 0, 0, 0.1); position: absolute; top: -25px; } diff --git a/src/components/UserAvatar/index.tsx b/src/components/UserAvatar/index.tsx index 9ae60ce..6049a07 100644 --- a/src/components/UserAvatar/index.tsx +++ b/src/components/UserAvatar/index.tsx @@ -30,7 +30,7 @@ export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => { padding: 0 }} /> - {name ? : null} + {name ? {name} : null} ) } diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index a544d0f..7da2b79 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -915,7 +915,7 @@ export const CreatePage = () => {
+ {isDragActive ? ( -

Drop the files here ...

+ ) : ( -

Click or drag files to upload!

+ )}
From b12ce258eb83620aa0dae546f146d381b9d61093 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 20 Aug 2024 09:44:04 +0200 Subject: [PATCH 55/63] fix(create-page): show other file types in content --- src/components/DrawPDFFields/index.tsx | 37 ++++++++++++------- .../DrawPDFFields/style.module.scss | 12 ++++++ src/utils/meta.ts | 8 ++++ 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 7e87cb3..8fb921d 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -21,7 +21,7 @@ import { DrawTool } from '../../types/drawing' import { truncate } from 'lodash' -import { hexToNpub } from '../../utils' +import { extractFileExtension, hexToNpub } from '../../utils' import { toPdfFiles } from '../../utils/pdf.ts' PDFJS.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -478,25 +478,34 @@ export const DrawPDFFields = (props: Props) => { return (
- {pdfFiles.map((pdfFile, pdfFileIndex: number) => { + {selectedFiles.map((file, i) => { + const name = file.name + const extension = extractFileExtension(name) + const pdfFile = pdfFiles.find((pdf) => pdf.file.name === name) return ( - +
- {getPdfPages(pdfFile, pdfFileIndex)} - {pdfFileIndex < pdfFiles.length - 1 && ( - - File Separator - + {pdfFile ? ( + getPdfPages(pdfFile, i) + ) : ( +
+ This is a {extension} file +
)}
+ {i < selectedFiles.length - 1 && ( + + File Separator + + )}
) })} diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 8c888ec..142f88a 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -104,3 +104,15 @@ 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/utils/meta.ts b/src/utils/meta.ts index 276f049..0bee969 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -190,3 +190,11 @@ export const extractFileExtensions = (fileNames: string[]) => { return { extensions, isSame } } + +/** + * @param fileName - Filename to check + * @returns Extension string + */ +export const extractFileExtension = (fileName: string) => { + return fileName.split('.').pop() +} From 55bf90e93c05b06467bc70199ebc36de4d3950f9 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 20 Aug 2024 14:21:45 +0300 Subject: [PATCH 56/63] chore: linting --- src/components/DrawPDFFields/index.tsx | 73 ++++++++++++++++---------- src/components/MarkFormField/index.tsx | 12 +++-- 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index e98187c..5318361 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -49,7 +49,7 @@ interface Props { } export const DrawPDFFields = (props: Props) => { - const { selectedFiles } = props + const { selectedFiles, onDrawFieldsChange, users } = props const [pdfFiles, setPdfFiles] = useState([]) const [parsingPdf, setParsingPdf] = useState(false) @@ -94,6 +94,15 @@ export const DrawPDFFields = (props: Props) => { }) useEffect(() => { + /** + * Reads the pdf binary files and converts it's pages to images + * creates the pdfFiles object and sets to a state + */ + const parsePdfPages = async () => { + const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles) + + setPdfFiles(pdfFiles) + } if (selectedFiles) { setParsingPdf(true) @@ -104,8 +113,8 @@ export const DrawPDFFields = (props: Props) => { }, [selectedFiles]) useEffect(() => { - if (pdfFiles) props.onDrawFieldsChange(pdfFiles) - }, [pdfFiles]) + if (pdfFiles) onDrawFieldsChange(pdfFiles) + }, [onDrawFieldsChange, pdfFiles]) /** * Drawing events @@ -132,12 +141,16 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param page PdfPage where press happened */ - const onMouseDown = (event: any, page: PdfPage) => { + const onMouseDown = ( + event: React.MouseEvent, + page: PdfPage + ) => { // Proceed only if left click if (event.button !== 0) return // Only allow drawing if mouse is not over other drawn element - const isOverPdfImageWrapper = event.target.tagName === 'IMG' + const target = event.target as HTMLElement + const isOverPdfImageWrapper = target.tagName === 'IMG' if (!selectedTool || !isOverPdfImageWrapper) { return @@ -185,7 +198,10 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param page PdfPage where moving is happening */ - const onMouseMove = (event: any, page: PdfPage) => { + const onMouseMove = ( + event: React.MouseEvent, + page: PdfPage + ) => { if (mouseState.clicked && selectedTool) { const lastElementIndex = page.drawnFields.length - 1 const lastDrawnField = page.drawnFields[lastElementIndex] @@ -216,7 +232,7 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param drawnField Which we are moving */ - const onDrawnFieldMouseDown = (event: any) => { + const onDrawnFieldMouseDown = (event: React.MouseEvent) => { event.stopPropagation() // Proceed only if left click @@ -239,11 +255,15 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param drawnField which we are moving */ - const onDranwFieldMouseMove = (event: any, drawnField: DrawnField) => { + const onDranwFieldMouseMove = ( + event: React.MouseEvent, + drawnField: DrawnField + ) => { + const target = event.target as HTMLElement | null if (mouseState.dragging) { const { mouseX, mouseY, rect } = getMouseCoordinates( event, - event.target.parentNode + target?.parentNode as HTMLElement ) const coordsOffset = mouseState.coordsInWrapper @@ -272,7 +292,7 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param drawnField which we are resizing */ - const onResizeHandleMouseDown = (event: any) => { + const onResizeHandleMouseDown = (event: React.MouseEvent) => { // Proceed only if left click if (event.button !== 0) return @@ -288,11 +308,15 @@ export const DrawPDFFields = (props: Props) => { * @param event Mouse event * @param drawnField which we are resizing */ - const onResizeHandleMouseMove = (event: any, drawnField: DrawnField) => { + const onResizeHandleMouseMove = ( + event: React.MouseEvent, + drawnField: DrawnField + ) => { + const target = event.target as HTMLElement | null if (mouseState.resizing) { const { mouseX, mouseY } = getMouseCoordinates( event, - event.target.parentNode.parentNode + target?.parentNode?.parentNode as HTMLElement ) const width = mouseX - drawnField.left @@ -313,7 +337,7 @@ export const DrawPDFFields = (props: Props) => { * @param drawnFileIndex drawn file index */ const onRemoveHandleMouseDown = ( - event: any, + event: React.MouseEvent, pdfFileIndex: number, pdfPageIndex: number, drawnFileIndex: number @@ -331,7 +355,9 @@ export const DrawPDFFields = (props: Props) => { * so select can work properly * @param event Mouse event */ - const onUserSelectHandleMouseDown = (event: any) => { + const onUserSelectHandleMouseDown = ( + event: React.MouseEvent + ) => { event.stopPropagation() } @@ -341,8 +367,11 @@ export const DrawPDFFields = (props: Props) => { * @param customTarget mouse coordinates relative to this element, if not provided * event.target will be used */ - const getMouseCoordinates = (event: any, customTarget?: any) => { - const target = customTarget ? customTarget : event.target + const getMouseCoordinates = ( + event: React.MouseEvent, + customTarget?: HTMLElement + ) => { + const target = (customTarget ? customTarget : event.target) as HTMLElement 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. @@ -354,16 +383,6 @@ export const DrawPDFFields = (props: Props) => { } } - /** - * Reads the pdf binary files and converts it's pages to images - * creates the pdfFiles object and sets to a state - */ - const parsePdfPages = async () => { - const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles) - - setPdfFiles(pdfFiles) - } - /** * * @returns if expanded pdf accordion is present @@ -477,7 +496,7 @@ export const DrawPDFFields = (props: Props) => { labelId="counterparts" label="Counterparts" > - {props.users.map((user, index) => { + {users.map((user, index) => { let displayValue = truncate( hexToNpub(user.pubkey), { diff --git a/src/components/MarkFormField/index.tsx b/src/components/MarkFormField/index.tsx index 2fa2780..e1003a0 100644 --- a/src/components/MarkFormField/index.tsx +++ b/src/components/MarkFormField/index.tsx @@ -7,15 +7,17 @@ import { isCurrentUserMarksComplete, isCurrentValueLast } from '../../utils' -import { useState } from 'react' +import React, { useState } from 'react' interface MarkFormFieldProps { - handleSubmit: (event: any) => void - handleSelectedMarkValueChange: (event: any) => void - selectedMark: CurrentUserMark - selectedMarkValue: string currentUserMarks: CurrentUserMark[] handleCurrentUserMarkChange: (mark: CurrentUserMark) => void + handleSelectedMarkValueChange: ( + event: React.ChangeEvent + ) => void + handleSubmit: (event: React.FormEvent) => void + selectedMark: CurrentUserMark + selectedMarkValue: string } /** From 3a6c5ae8e39a65853424d7cd7a6dc18d7e5c7bab Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 20 Aug 2024 14:27:35 +0300 Subject: [PATCH 57/63] chore: formatting --- src/types/mark.ts | 2 +- src/utils/pdf.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/mark.ts b/src/types/mark.ts index 1e6039b..efc1899 100644 --- a/src/types/mark.ts +++ b/src/types/mark.ts @@ -1,4 +1,4 @@ -import { MarkType } from "./drawing"; +import { MarkType } from './drawing' export interface CurrentUserMark { id: number diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index cef12c2..622a259 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -260,4 +260,4 @@ export { addMarks, convertToPdfBlob, groupMarksByPage -} \ No newline at end of file +} From e0d6c0363951c66bdede17759ae712626a4a07a5 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 20 Aug 2024 13:52:24 +0200 Subject: [PATCH 58/63] fix(relay-controller): sigit relay immutability and relay list --- src/controllers/RelayController.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/controllers/RelayController.ts b/src/controllers/RelayController.ts index 12f24c2..83d8ab3 100644 --- a/src/controllers/RelayController.ts +++ b/src/controllers/RelayController.ts @@ -86,11 +86,8 @@ export class RelayController { filter: Filter, relayUrls: string[] = [] ): Promise => { - // add app relay to relays array - relayUrls.push(SIGIT_RELAY) - - // Connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => + // Add app relay to relays array and connect to all specified relays + const relayPromises = [...relayUrls, SIGIT_RELAY].map((relayUrl) => this.connectRelay(relayUrl) ) @@ -204,11 +201,9 @@ export class RelayController { relayUrls: string[] = [], eventHandler: (event: Event) => void ) => { - // add app relay to relays array - relayUrls.push(SIGIT_RELAY) + // Add app relay to relays array and connect to all specified relays - // Connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => + const relayPromises = [...relayUrls, SIGIT_RELAY].map((relayUrl) => this.connectRelay(relayUrl) ) @@ -263,11 +258,9 @@ export class RelayController { event: Event, relayUrls: string[] = [] ): Promise => { - // add app relay to relays array - relayUrls.push(SIGIT_RELAY) + // Add app relay to relays array and connect to all specified relays - // Connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => + const relayPromises = [...relayUrls, SIGIT_RELAY].map((relayUrl) => this.connectRelay(relayUrl) ) From bec3c92b03c4127c9f768770640f6c41d0c329be Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 20 Aug 2024 14:28:34 +0200 Subject: [PATCH 59/63] fix: add missing null and reduce warning limit --- package.json | 2 +- src/components/DrawPDFFields/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 88a1439..f1c513c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 14", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 10", "lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint:staged": "eslint --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "formatter:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"", diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 8b8a2a2..d0c4b61 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -328,7 +328,7 @@ export const DrawPDFFields = (props: Props) => { */ const getMouseCoordinates = ( event: React.MouseEvent, - customTarget?: HTMLElement + customTarget?: HTMLElement | null ) => { const target = customTarget ? customTarget : event.currentTarget const rect = target.getBoundingClientRect() From 61f39d17ff4619c2b6beedf7d3189c03278aeede Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 20 Aug 2024 14:38:03 +0200 Subject: [PATCH 60/63] fix(lint): add deps, remove any, update warning limit --- package.json | 2 +- src/pages/sign/index.tsx | 6 ++++-- src/pages/sign/internal/displayMeta.tsx | 2 +- src/types/errors/DecryptionError.ts | 24 ++++++++++++++++-------- src/utils/zip.ts | 6 ++++-- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index f1c513c..758d63d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 10", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 6", "lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint:staged": "eslint --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "formatter:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"", diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index a762292..be755f4 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -236,9 +236,11 @@ export const SignPage = () => { if (!arrayBuffer) return const blob = new Blob([arrayBuffer]) saveAs(blob, `exported-${unixNow()}.sigit.zip`) - } catch (error: any) { + } catch (error) { console.log('error in zip:>> ', error) - toast.error(error.message || 'Error occurred in generating zip file') + if (error instanceof Error) { + toast.error(error.message || 'Error occurred in generating zip file') + } } } diff --git a/src/pages/sign/internal/displayMeta.tsx b/src/pages/sign/internal/displayMeta.tsx index fbc9264..03ba364 100644 --- a/src/pages/sign/internal/displayMeta.tsx +++ b/src/pages/sign/internal/displayMeta.tsx @@ -141,7 +141,7 @@ export const DisplayMeta = ({ }) } }) - }, [users, submittedBy]) + }, [users, submittedBy, metadata]) const downloadFile = async (filename: string) => { const arrayBuffer = await files[filename].file.arrayBuffer() diff --git a/src/types/errors/DecryptionError.ts b/src/types/errors/DecryptionError.ts index 6a174b8..7014ab8 100644 --- a/src/types/errors/DecryptionError.ts +++ b/src/types/errors/DecryptionError.ts @@ -1,18 +1,26 @@ export class DecryptionError extends Error { public message: string = '' - constructor(public inputError: any) { + constructor(public inputError: unknown) { super() - if (inputError.message.toLowerCase().includes('expected')) { - this.message = `The decryption key length or format is invalid.` - } else if ( - inputError.message.includes('The JWK "alg" member was inconsistent') - ) { - this.message = `The decryption key is invalid.` + // Make sure inputError has access to the .message + if (inputError instanceof Error) { + if (inputError.message.toLowerCase().includes('expected')) { + this.message = `The decryption key length or format is invalid.` + } else if ( + inputError.message.includes('The JWK "alg" member was inconsistent') + ) { + this.message = `The decryption key is invalid.` + } else { + this.message = + inputError.message || 'An error occurred while decrypting file.' + } } else { + // We don't have message on the inputError + // Stringify whole error and set that as a message this.message = - inputError.message || 'An error occurred while decrypting file.' + JSON.stringify(inputError) || 'An error occurred while decrypting file.' } this.name = 'DecryptionError' diff --git a/src/utils/zip.ts b/src/utils/zip.ts index 7a7a49f..577dd86 100644 --- a/src/utils/zip.ts +++ b/src/utils/zip.ts @@ -37,9 +37,11 @@ const readContentOfZipEntry = async ( const loadZip = async (data: InputFileFormat): Promise => { try { return await JSZip.loadAsync(data) - } catch (err: any) { + } catch (err) { console.log('err in loading zip file :>> ', err) - toast.error(err.message || 'An error occurred in loading zip file.') + if (err instanceof Error) { + toast.error(err.message || 'An error occurred in loading zip file.') + } return null } } From 404f4aa3a1e8db7ff90300b107496f59a28a539e Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 20 Aug 2024 14:40:12 +0200 Subject: [PATCH 61/63] fix(lint): update warning limit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 758d63d..dc13581 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 6", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 2", "lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint:staged": "eslint --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "formatter:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"", From c3a39157ffdb217dc9e7fa16ff600386a1f95307 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 20 Aug 2024 15:47:51 +0200 Subject: [PATCH 62/63] fix(verify-page): remove mark border in production, enable dev flag for css classes Hotfix --- src/layouts/Main.tsx | 11 ++++++++++- src/pages/verify/style.module.scss | 7 ++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index 7b34720..ac233cc 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -145,10 +145,19 @@ export const MainLayout = () => { if (isLoading) return + const isDev = import.meta.env.MODE === 'development' + return ( <> -
+