From 4bc5882ab60cd4b31f532d506433d24c67548082 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 11 Sep 2024 13:27:50 +0200 Subject: [PATCH 01/18] fix(loading): make sure the default spinner is absolute relative to root always --- src/components/LoadingSpinner/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/LoadingSpinner/index.tsx b/src/components/LoadingSpinner/index.tsx index 2c6f4e5..648eb9a 100644 --- a/src/components/LoadingSpinner/index.tsx +++ b/src/components/LoadingSpinner/index.tsx @@ -1,3 +1,4 @@ +import { createPortal } from 'react-dom' import styles from './style.module.scss' interface Props { @@ -20,7 +21,7 @@ export const LoadingSpinner = (props: Props) => { ) default: - return ( + return createPortal(
{
{desc &&

{desc}

}
-
+ , + document.getElementById('root')! ) } } From 4d1e6722681849c72d8ad5cfaebc543ab61dd907 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 11 Sep 2024 16:33:13 +0200 Subject: [PATCH 02/18] feat(loading-spinner): add children support for default variant --- src/components/LoadingSpinner/index.tsx | 12 +++++++++--- src/components/LoadingSpinner/style.module.scss | 6 +++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/components/LoadingSpinner/index.tsx b/src/components/LoadingSpinner/index.tsx index 648eb9a..d19d01f 100644 --- a/src/components/LoadingSpinner/index.tsx +++ b/src/components/LoadingSpinner/index.tsx @@ -1,13 +1,14 @@ import { createPortal } from 'react-dom' import styles from './style.module.scss' +import { PropsWithChildren } from 'react' interface Props { desc?: string variant?: 'small' | 'default' } -export const LoadingSpinner = (props: Props) => { - const { desc, variant = 'default' } = props +export const LoadingSpinner = (props: PropsWithChildren) => { + const { desc, children, variant = 'default' } = props switch (variant) { case 'small': @@ -28,7 +29,12 @@ export const LoadingSpinner = (props: Props) => { data-variant={variant} >
- {desc &&

{desc}

} + {desc && ( +
+ {desc} + {children} +
+ )} , document.getElementById('root')! diff --git a/src/components/LoadingSpinner/style.module.scss b/src/components/LoadingSpinner/style.module.scss index e1a5978..d51b743 100644 --- a/src/components/LoadingSpinner/style.module.scss +++ b/src/components/LoadingSpinner/style.module.scss @@ -42,11 +42,15 @@ width: 100%; padding: 15px; border-top: solid 1px rgba(0, 0, 0, 0.1); - text-align: center; color: rgba(0, 0, 0, 0.5); font-size: 16px; font-weight: 400; + + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; } @keyframes spin { From 9c545a477cf5e6e9ecf8f171af3857fd64b5a78d Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 11 Sep 2024 16:33:53 +0200 Subject: [PATCH 03/18] fix(errors): add custom timeout error --- src/types/errors/TimeoutError.ts | 6 ++++++ src/utils/utils.ts | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/types/errors/TimeoutError.ts diff --git a/src/types/errors/TimeoutError.ts b/src/types/errors/TimeoutError.ts new file mode 100644 index 0000000..5bd31c5 --- /dev/null +++ b/src/types/errors/TimeoutError.ts @@ -0,0 +1,6 @@ +export class TimeoutError extends Error { + constructor() { + super('Timeout') + this.name = this.constructor.name + } +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index f32e14e..ee21230 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,3 +1,4 @@ +import { TimeoutError } from '../types/errors/TimeoutError.ts' import { CurrentUserFile } from '../types/file.ts' import { SigitFile } from './file.ts' @@ -63,7 +64,7 @@ export const timeout = (ms: number = 60000) => { // Set a timeout using setTimeout setTimeout(() => { // Reject the promise with an Error indicating a timeout - reject(new Error('Timeout')) + reject(new TimeoutError()) }, ms) // Timeout duration in milliseconds }) } From 7c80643aba266fc0279c593f1951931f3fbb9ce2 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 11 Sep 2024 16:44:45 +0200 Subject: [PATCH 04/18] fix(login): extension login infinite loading Fixes #196 --- src/pages/nostr/index.tsx | 75 ++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/src/pages/nostr/index.tsx b/src/pages/nostr/index.tsx index bd99485..8726c6e 100644 --- a/src/pages/nostr/index.tsx +++ b/src/pages/nostr/index.tsx @@ -18,12 +18,16 @@ import { } from '../../store/actions' import { LoginMethods } from '../../store/auth/types' import { Dispatch } from '../../store/store' -import { npubToHex, queryNip05 } from '../../utils' +import { npubToHex, queryNip05, timeout } from '../../utils' import { hexToBytes } from '@noble/hashes/utils' import { NIP05_REGEX } from '../../constants' import styles from './styles.module.scss' +import { TimeoutError } from '../../types/errors/TimeoutError' +const EXTENSION_LOGIN_DELAY_SECONDS = 2 +const EXTENSION_LOGIN_TIMEOUT_SECONDS = EXTENSION_LOGIN_DELAY_SECONDS + 10 + export const Nostr = () => { const [searchParams] = useSearchParams() @@ -36,6 +40,7 @@ export const Nostr = () => { const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') + const [isExtensionSlow, setIsExtensionSlow] = useState(false) const [inputValue, setInputValue] = useState('') const [authUrl, setAuthUrl] = useState() @@ -72,27 +77,39 @@ export const Nostr = () => { } const loginWithExtension = async () => { - setIsLoading(true) - setLoadingSpinnerDesc('Capturing pubkey from nostr extension') + try { + // Wait EXTENSION_LOGIN_DELAY_SECONDS before showing extension delay message + const waitTimeout = window.setTimeout(() => { + setIsExtensionSlow(true) + }, 2000) - nostrController - .capturePublicKey() - .then(async (pubkey) => { - dispatch(updateLoginMethod(LoginMethods.extension)) + setIsLoading(true) + setLoadingSpinnerDesc('Capturing pubkey from nostr extension') - setLoadingSpinnerDesc('Authenticating and finding metadata') - const redirectPath = - await authController.authAndGetMetadataAndRelaysMap(pubkey) + const pubkey = await nostrController.capturePublicKey() + dispatch(updateLoginMethod(LoginMethods.extension)) - if (redirectPath) navigateAfterLogin(redirectPath) - }) - .catch((err) => { - toast.error('Error capturing public key from nostr extension: ' + err) - }) - .finally(() => { - setIsLoading(false) - setLoadingSpinnerDesc('') - }) + setLoadingSpinnerDesc('Authenticating and finding metadata') + const redirectPath = await Promise.race([ + authController.authAndGetMetadataAndRelaysMap(pubkey), + timeout(EXTENSION_LOGIN_TIMEOUT_SECONDS * 1000) + ]) + + if (redirectPath) { + window.clearTimeout(waitTimeout) + navigateAfterLogin(redirectPath) + } + } catch (error) { + if (error instanceof TimeoutError) { + toast.error("Extension didn't respond in time") + } else { + toast.error('Error capturing public key from nostr extension: ' + error) + } + } finally { + setIsLoading(false) + setLoadingSpinnerDesc('') + setIsExtensionSlow(false) + } } /** @@ -354,7 +371,25 @@ export const Nostr = () => { return ( <> - {isLoading && } + {isLoading && ( + + {isExtensionSlow && ( + <> +

Extension is not responding

+ + + )} +
+ )} {isNostrExtensionAvailable && ( <> From 9191336722c234eb1945e06230099051c7fc9df3 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 12 Sep 2024 12:17:58 +0200 Subject: [PATCH 05/18] refactor(login): update the delay message and increase timers --- src/pages/nostr/index.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pages/nostr/index.tsx b/src/pages/nostr/index.tsx index 8726c6e..557d9e3 100644 --- a/src/pages/nostr/index.tsx +++ b/src/pages/nostr/index.tsx @@ -25,8 +25,8 @@ import { NIP05_REGEX } from '../../constants' import styles from './styles.module.scss' import { TimeoutError } from '../../types/errors/TimeoutError' -const EXTENSION_LOGIN_DELAY_SECONDS = 2 -const EXTENSION_LOGIN_TIMEOUT_SECONDS = EXTENSION_LOGIN_DELAY_SECONDS + 10 +const EXTENSION_LOGIN_DELAY_SECONDS = 5 +const EXTENSION_LOGIN_TIMEOUT_SECONDS = EXTENSION_LOGIN_DELAY_SECONDS + 55 export const Nostr = () => { const [searchParams] = useSearchParams() @@ -375,7 +375,14 @@ export const Nostr = () => { {isExtensionSlow && ( <> -

Extension is not responding

+

+ Your nostr extension is not responding. Check these + alternatives:{' '} + + https://github.com/aljazceru/awesome-nostr + +

+
-
{toolbox.map((drawTool: DrawTool, index: number) => { @@ -987,6 +984,10 @@ export const CreatePage = () => { })}
+ + {!!error && ( {error} )} From ea7e3a096450940689a5fd24dacad27d4b006f91 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 13 Sep 2024 11:06:06 +0200 Subject: [PATCH 08/18] refactor: update url for online status --- src/utils/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ee21230..ece0770 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -35,7 +35,7 @@ export const isOnline = async () => { try { // Define a URL to check the online status - const url = 'https://www.google.com' + const url = 'https://www.sigit.io' // Make a HEAD request to the URL with 'no-cors' mode // This mode is used to handle opaque responses which do not expose their content From ba24e7417d99ba678b0eed4dd5e6c354e4a95921 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 13 Sep 2024 11:14:10 +0200 Subject: [PATCH 09/18] refactor: log timeout error only, no toast --- src/pages/nostr/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/nostr/index.tsx b/src/pages/nostr/index.tsx index 0467c9b..7933ba0 100644 --- a/src/pages/nostr/index.tsx +++ b/src/pages/nostr/index.tsx @@ -101,7 +101,8 @@ export const Nostr = () => { } } catch (error) { if (error instanceof TimeoutError) { - toast.error("Extension didn't respond in time") + // Just log the error, no toast, user has already been notified with the loading screen + console.error("Extension didn't respond in time") } else { toast.error('Error capturing public key from nostr extension: ' + error) } From 79ef9eb8d6ced4cbb0517def4b7864176c78b1f4 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 13 Sep 2024 17:47:55 +0200 Subject: [PATCH 10/18] fix: url --- src/utils/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ece0770..7691fc7 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -35,7 +35,7 @@ export const isOnline = async () => { try { // Define a URL to check the online status - const url = 'https://www.sigit.io' + const url = 'https://sigit.io' // Make a HEAD request to the URL with 'no-cors' mode // This mode is used to handle opaque responses which do not expose their content From 8b4f1a8973abe0a395cacf9617aab80bb606bf61 Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 16 Sep 2024 11:00:16 +0200 Subject: [PATCH 11/18] fix(online-detection): use relative url --- src/utils/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 7691fc7..11053b9 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -35,7 +35,7 @@ export const isOnline = async () => { try { // Define a URL to check the online status - const url = 'https://sigit.io' + const url = document.location.pathname + '?v=' + new Date().getTime() // Make a HEAD request to the URL with 'no-cors' mode // This mode is used to handle opaque responses which do not expose their content From 759a40a4f910d81fa95bc4b7304bc6a1eb6b8eda Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 13 Sep 2024 17:53:22 +0200 Subject: [PATCH 12/18] fix(verify): offline flow --- src/pages/verify/index.tsx | 189 +++++++++---------------------------- 1 file changed, 46 insertions(+), 143 deletions(-) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index e869c98..af0b4c7 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -1,16 +1,11 @@ -import { Box, Button, Tooltip, Typography } from '@mui/material' +import { Box, Button, Typography } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' -import { Event, verifyEvent } from 'nostr-tools' import { useEffect, useRef, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { NostrController } from '../../controllers' -import { - CreateSignatureEventContent, - DocSignatureEvent, - Meta -} from '../../types' +import { DocSignatureEvent, Meta } from '../../types' import { decryptArrayBuffer, extractMarksFromSignedMeta, @@ -20,7 +15,6 @@ import { parseJson, readContentOfZipEntry, signEventForMetaFile, - shorten, getCurrentUserFiles } from '../../utils' import styles from './style.module.scss' @@ -42,9 +36,6 @@ import { Container } from '../../components/Container' import { useSigitMeta } from '../../hooks/useSigitMeta.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' import FileList from '../../components/FileList' import { CurrentUserFile } from '../../types/file.ts' import { Mark } from '../../types/mark.ts' @@ -163,12 +154,26 @@ const SlimPdfView = ({ export const VerifyPage = () => { const location = useLocation() + const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) + + const nostrController = NostrController.getInstance() + + const [isLoading, setIsLoading] = useState(false) + const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') + /** * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json * meta will be received in navigation from create & home page in online mode */ - const { uploadedZip, meta } = location.state || {} + const { uploadedZip, meta: metaInNavState } = location.state || {} + const [selectedFile, setSelectedFile] = useState(null) + useEffect(() => { + if (uploadedZip) { + setSelectedFile(uploadedZip) + } + }, [uploadedZip]) + const [meta, setMeta] = useState(metaInNavState) const { submittedBy, zipUrl, @@ -179,44 +184,22 @@ export const VerifyPage = () => { parsedSignatureEvents } = useSigitMeta(meta) - const profiles = useSigitProfiles([ - ...(submittedBy ? [submittedBy] : []), - ...signers, - ...viewers - ]) - - const [isLoading, setIsLoading] = useState(false) - const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') - - const [selectedFile, setSelectedFile] = useState(null) + const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({}) + const [currentFile, setCurrentFile] = useState(null) const [currentFileHashes, setCurrentFileHashes] = useState<{ [key: string]: string | null }>({}) - const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({}) - 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, signatureFileHashes) + const tmp = getCurrentUserFiles(files, currentFileHashes, fileHashes) setCurrentFile(tmp[0]) } - }, [signatureFileHashes, fileHashes, files]) - - const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) - const nostrController = NostrController.getInstance() + }, [currentFileHashes, fileHashes, files]) useEffect(() => { - if (uploadedZip) { - setSelectedFile(uploadedZip) - } else if (meta && encryptionKey) { + if (metaInNavState && encryptionKey) { const processSigit = async () => { setIsLoading(true) @@ -301,7 +284,7 @@ export const VerifyPage = () => { processSigit() } - }, [encryptionKey, meta, uploadedZip, zipUrl]) + }, [encryptionKey, metaInNavState, zipUrl]) const handleVerify = async () => { if (!selectedFile) return @@ -315,6 +298,7 @@ export const VerifyPage = () => { if (!zip) return + const files: { [filename: string]: SigitFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files) .filter((entry) => entry.name.startsWith('files/') && !entry.dir) @@ -322,24 +306,27 @@ export const VerifyPage = () => { // generate hashes for all entries in files folder of zipArchive // these hashes can be used to verify the originality of files - for (const fileName of fileNames) { + for (const zipFilePath of fileNames) { const arrayBuffer = await readContentOfZipEntry( zip, - fileName, + zipFilePath, 'arraybuffer' ) + const fileName = zipFilePath.replace(/^files\//, '') if (arrayBuffer) { + files[fileName] = await convertToSigitFile(arrayBuffer, fileName) const hash = await getHash(arrayBuffer) if (hash) { - fileHashes[fileName.replace(/^files\//, '')] = hash + fileHashes[fileName] = hash } } else { - fileHashes[fileName.replace(/^files\//, '')] = null + fileHashes[fileName] = null } } + setFiles(files) setCurrentFileHashes(fileHashes) setLoadingSpinnerDesc('Parsing meta.json') @@ -368,43 +355,7 @@ export const VerifyPage = () => { if (!parsedMetaJson) return - const createSignatureEvent = await parseJson( - parsedMetaJson.createSignature - ).catch((err) => { - console.log('err in parsing the createSignature event:>> ', err) - toast.error( - err.message || 'error occurred in parsing the create signature event' - ) - setIsLoading(false) - return null - }) - - if (!createSignatureEvent) return - - const isValidCreateSignature = verifyEvent(createSignatureEvent) - - if (!isValidCreateSignature) { - toast.error('Create signature is invalid') - setIsLoading(false) - return - } - - const createSignatureContent = await parseJson( - createSignatureEvent.content - ).catch((err) => { - console.log( - `err in parsing the createSignature event's content :>> `, - err - ) - toast.error( - err.message || - `error occurred in parsing the create signature event's content` - ) - setIsLoading(false) - return null - }) - - if (!createSignatureContent) return + setMeta(parsedMetaJson) setIsLoading(false) } @@ -479,47 +430,6 @@ export const VerifyPage = () => { setIsLoading(false) } - const displayExportedBy = () => { - if (!meta || !meta.exportSignature) return null - - const exportSignatureString = meta.exportSignature - - try { - const exportSignatureEvent = JSON.parse(exportSignatureString) as Event - - if (verifyEvent(exportSignatureEvent)) { - const exportedBy = exportSignatureEvent.pubkey - const profile = profiles[exportedBy] - return ( - - - - - - ) - } else { - toast.error(`Invalid export signature!`) - return ( - - Invalid export signature - - ) - } - } catch (error) { - console.error(`An error occurred wile parsing exportSignature`, error) - return null - } - } - return ( <> {isLoading && } @@ -554,22 +464,19 @@ export const VerifyPage = () => { {meta && ( - {currentFile !== null && ( - - )} - {displayExportedBy()} - + currentFile !== null && ( + + ) } right={} leftIcon={faFileDownload} @@ -578,11 +485,7 @@ export const VerifyPage = () => { > From 1dfab7b82b6df34d7ae3cccdb3251498d6475732 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 13 Sep 2024 17:57:19 +0200 Subject: [PATCH 13/18] refactor: metadatacontroller as singleton --- src/components/AppBar/AppBar.tsx | 2 +- src/controllers/AuthController.ts | 2 +- src/controllers/MetadataController.ts | 9 ++++++++- src/layouts/Main.tsx | 2 +- src/pages/nostr/index.tsx | 2 +- src/pages/profile/index.tsx | 2 +- src/pages/settings/profile/index.tsx | 4 ++-- src/utils/dvm.ts | 2 +- src/utils/nostr.ts | 6 +++--- 9 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 8c7ac63..aaabbd3 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -37,7 +37,7 @@ import { setUserRobotImage } from '../../store/userRobotImage/action' import { Container } from '../Container' import { ButtonIcon } from '../ButtonIcon' -const metadataController = new MetadataController() +const metadataController = MetadataController.getInstance() export const AppBar = () => { const navigate = useNavigate() diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 09b20df..cc8def5 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -25,7 +25,7 @@ export class AuthController { constructor() { this.nostrController = NostrController.getInstance() - this.metadataController = new MetadataController() + this.metadataController = MetadataController.getInstance() } /** diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index 8053874..984afd3 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -22,6 +22,7 @@ import { import { DEFAULT_LOOK_UP_RELAY_LIST } from '../utils/const' export class MetadataController extends EventEmitter { + private static instance: MetadataController private nostrController: NostrController private specialMetadataRelay = 'wss://purplepag.es' private pendingFetches = new Map>() // Track pending fetches @@ -31,6 +32,13 @@ export class MetadataController extends EventEmitter { this.nostrController = NostrController.getInstance() } + public static getInstance(): MetadataController { + if (!MetadataController.instance) { + MetadataController.instance = new MetadataController() + } + return MetadataController.instance + } + /** * Asynchronously checks for more recent metadata events authored by a specific key. * If a more recent metadata event is found, it is handled and returned. @@ -119,7 +127,6 @@ export class MetadataController extends EventEmitter { // Check if the cached metadata is older than one day if (isOlderThanOneDay(cachedMetadataEvent.cachedAt)) { // If older than one week, find the metadata from relays in background - this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event) } diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index 9402e97..3a1f10e 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -38,7 +38,7 @@ export const MainLayout = () => { const hasSubscribed = useRef(false) useEffect(() => { - const metadataController = new MetadataController() + const metadataController = MetadataController.getInstance() const logout = () => { dispatch( diff --git a/src/pages/nostr/index.tsx b/src/pages/nostr/index.tsx index bd99485..a9948af 100644 --- a/src/pages/nostr/index.tsx +++ b/src/pages/nostr/index.tsx @@ -31,7 +31,7 @@ export const Nostr = () => { const navigate = useNavigate() const authController = new AuthController() - const metadataController = new MetadataController() + const metadataController = MetadataController.getInstance() const nostrController = NostrController.getInstance() const [isLoading, setIsLoading] = useState(false) diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index ca4bb87..905e7d0 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -27,7 +27,7 @@ export const ProfilePage = () => { const { npub } = useParams() - const metadataController = useMemo(() => new MetadataController(), []) + const metadataController = useMemo(() => MetadataController.getInstance(), []) const [pubkey, setPubkey] = useState() const [nostrJoiningBlock, setNostrJoiningBlock] = diff --git a/src/pages/settings/profile/index.tsx b/src/pages/settings/profile/index.tsx index 9b01a72..245ae77 100644 --- a/src/pages/settings/profile/index.tsx +++ b/src/pages/settings/profile/index.tsx @@ -12,7 +12,7 @@ import { useTheme } from '@mui/material' import { UnsignedEvent, nip19, kinds, VerifiedEvent, Event } from 'nostr-tools' -import React, { useEffect, useMemo, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { toast } from 'react-toastify' import { MetadataController, NostrController } from '../../../controllers' @@ -41,7 +41,7 @@ export const ProfileSettingsPage = () => { const dispatch: Dispatch = useDispatch() - const metadataController = useMemo(() => new MetadataController(), []) + const metadataController = MetadataController.getInstance() const nostrController = NostrController.getInstance() const [pubkey, setPubkey] = useState() diff --git a/src/utils/dvm.ts b/src/utils/dvm.ts index 8995ae7..c2f33e8 100644 --- a/src/utils/dvm.ts +++ b/src/utils/dvm.ts @@ -13,7 +13,7 @@ import { setRelayInfoAction } from '../store/actions' export const getNostrJoiningBlockNumber = async ( hexKey: string ): Promise => { - const metadataController = new MetadataController() + const metadataController = MetadataController.getInstance() const relaySet = await metadataController.findRelayListMetadata(hexKey) diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index 0a3e052..a2799a3 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -365,7 +365,7 @@ export const getUsersAppData = async (): Promise => { // 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 metadataController = MetadataController.getInstance() const relaySet = await metadataController .findRelayListMetadata(usersPubkey) .catch((err) => { @@ -835,7 +835,7 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => { */ export const subscribeForSigits = async (pubkey: string) => { // Instantiate the MetadataController to retrieve relay list metadata - const metadataController = new MetadataController() + const metadataController = MetadataController.getInstance() const relaySet = await metadataController .findRelayListMetadata(pubkey) .catch((err) => { @@ -939,7 +939,7 @@ export const sendNotification = async (receiver: string, meta: Meta) => { const wrappedEvent = createWrap(unsignedEvent, receiver) // Instantiate the MetadataController to retrieve relay list metadata - const metadataController = new MetadataController() + const metadataController = MetadataController.getInstance() const relaySet = await metadataController .findRelayListMetadata(receiver) .catch((err) => { From 13254fbe0641796eb40425d0910db2d9fc43645d Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 13 Sep 2024 17:57:50 +0200 Subject: [PATCH 14/18] feat: add exportedBy to useSigitMeta --- src/hooks/useSigitMeta.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index 088940e..22a6a6d 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -33,6 +33,10 @@ export interface FlatMeta // Remove pubkey and use submittedBy as `npub1${string}` submittedBy?: `npub1${string}` + // Optional field only present on exported sigits + // Exporting adds user's pubkey + exportedBy?: `npub1${string}` + // Remove created_at and replace with createdAt createdAt?: number @@ -68,6 +72,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { const [tags, setTags] = useState() const [createdAt, setCreatedAt] = useState() const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event + const [exportedBy, setExportedBy] = useState<`npub1${string}`>() // pubkey from export signature nostr event const [id, setId] = useState() const [sig, setSig] = useState() @@ -99,6 +104,18 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { if (!meta) return ;(async function () { try { + if (meta.exportSignature) { + const exportSignatureEvent = await parseNostrEvent( + meta.exportSignature + ) + if ( + verifyEvent(exportSignatureEvent) && + exportSignatureEvent.pubkey + ) { + setExportedBy(exportSignatureEvent.pubkey as `npub1${string}`) + } + } + const createSignatureEvent = await parseNostrEvent(meta.createSignature) const { kind, tags, created_at, pubkey, id, sig, content } = @@ -265,6 +282,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { tags, createdAt, submittedBy, + exportedBy, id, sig, signers, From 8b00ef538b164b1116095fd5ffee95a5791667e5 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 13 Sep 2024 17:59:58 +0200 Subject: [PATCH 15/18] fix: unzip and use timeout util --- src/pages/sign/index.tsx | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 8720954..63e71dd 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -33,7 +33,8 @@ import { sendNotification, signEventForMetaFile, updateUsersAppData, - findOtherUserMarks + findOtherUserMarks, + timeout } from '../../utils' import { Container } from '../../components/Container' import { DisplayMeta } from './internal/displayMeta' @@ -276,17 +277,10 @@ export const SignPage = () => { setAuthUrl(url) }) - // Set up timeout promise to handle encryption timeout - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { - reject(new Error('Timeout occurred')) - }, 60000) // Timeout duration = 60 seconds - }) - - // decrypt the encryptionKey, with timeout + // decrypt the encryptionKey, with timeout (duration = 60 seconds) const encryptionKey = await Promise.race([ nostrController.nip04Decrypt(sender, key), - timeoutPromise + timeout(60000) ]) .then((res) => { return res @@ -468,20 +462,20 @@ export const SignPage = () => { const fileNames = Object.values(zip.files) .filter((entry) => entry.name.startsWith('files/') && !entry.dir) .map((entry) => entry.name) - .map((entry) => entry.replace(/^files\//, '')) - // generate hashes for all entries in files folder of zipArchive - // these hashes can be used to verify the originality of files - for (const fileName of fileNames) { + for (const zipFilePath of fileNames) { const arrayBuffer = await readContentOfZipEntry( zip, - fileName, + zipFilePath, 'arraybuffer' ) + const fileName = zipFilePath.replace(/^files\//, '') if (arrayBuffer) { files[fileName] = await convertToSigitFile(arrayBuffer, fileName) + // generate hashes for all entries in files folder of zipArchive + // these hashes can be used to verify the originality of files const hash = await getHash(arrayBuffer) if (hash) { fileHashes[fileName] = hash From e1c750495efe8d1875426931bc93ac7c2dd713f0 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 13 Sep 2024 18:13:34 +0200 Subject: [PATCH 16/18] refactor: useSigitProfiles removed, per user profile hook in avatar, refactor name tooltip --- src/components/DisplaySigit/index.tsx | 67 ++++------------- src/components/DisplaySigner/index.tsx | 48 ++++++------ src/components/UserAvatar/index.tsx | 48 ++++++++---- src/components/UsersDetails.tsx/index.tsx | 91 +++++------------------ src/hooks/useProfileMetadata.tsx | 46 ++++++++++++ src/hooks/useSigitProfiles.tsx | 71 ------------------ src/pages/create/index.tsx | 37 +++------ src/pages/sign/internal/displayMeta.tsx | 31 +------- 8 files changed, 148 insertions(+), 291 deletions(-) create mode 100644 src/hooks/useProfileMetadata.tsx delete mode 100644 src/hooks/useSigitProfiles.tsx diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index 473a942..62f397c 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -1,7 +1,7 @@ import { Meta } from '../../types' import { SigitCardDisplayInfo, SigitStatus, SignStatus } from '../../utils' import { Link } from 'react-router-dom' -import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils' +import { formatTimestamp, npubToHex } from '../../utils' import { appPublicRoutes, appPrivateRoutes } from '../../routes' import { Button, Divider, Tooltip } from '@mui/material' import { DisplaySigner } from '../DisplaySigner' @@ -17,9 +17,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { UserAvatarGroup } from '../UserAvatarGroup' import styles from './style.module.scss' -import { TooltipChild } from '../TooltipChild' import { getExtensionIconLabel } from '../getExtensionIconLabel' -import { useSigitProfiles } from '../../hooks/useSigitProfiles' import { useSigitMeta } from '../../hooks/useSigitMeta' import { extractFileExtensions } from '../../utils/file' @@ -33,12 +31,6 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { parsedMeta const { signersStatus, fileHashes } = useSigitMeta(meta) - - const profiles = useSigitProfiles([ - ...(submittedBy ? [submittedBy] : []), - ...signers - ]) - const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes)) return ( @@ -54,62 +46,29 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { >

{title}

- {submittedBy && - (function () { - const profile = profiles[submittedBy] - return ( - - - - - - ) - })()} + {submittedBy && ( + + )} {submittedBy && signers.length ? ( ) : null} {signers.map((signer) => { const pubkey = npubToHex(signer)! - const profile = profiles[pubkey] - return ( - - - - - + ) })}
-
+
{createdAt ? formatTimestamp(createdAt) : null}
diff --git a/src/components/DisplaySigner/index.tsx b/src/components/DisplaySigner/index.tsx index 63aa154..e05dcae 100644 --- a/src/components/DisplaySigner/index.tsx +++ b/src/components/DisplaySigner/index.tsx @@ -1,5 +1,4 @@ import { Badge } from '@mui/material' -import { ProfileMetadata } from '../../types' import styles from './style.module.scss' import { UserAvatar } from '../UserAvatar' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -15,38 +14,33 @@ import { SignStatus } from '../../utils' import { Spinner } from '../Spinner' type DisplaySignerProps = { - profile: ProfileMetadata pubkey: string status: SignStatus } -export const DisplaySigner = ({ - status, - profile, - pubkey -}: DisplaySignerProps) => { - 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 +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 - default: - return - } + default: + return } +} +export const DisplaySigner = ({ status, pubkey }: DisplaySignerProps) => { return ( {getStatusIcon(status)}
} > - + ) } diff --git a/src/components/UserAvatar/index.tsx b/src/components/UserAvatar/index.tsx index 6049a07..0ea1fc1 100644 --- a/src/components/UserAvatar/index.tsx +++ b/src/components/UserAvatar/index.tsx @@ -3,34 +3,56 @@ import { getProfileRoute } from '../../routes' import styles from './styles.module.scss' import { AvatarIconButton } from '../UserAvatarIconButton' import { Link } from 'react-router-dom' +import { useProfileMetadata } from '../../hooks/useProfileMetadata' +import { Tooltip } from '@mui/material' +import { shorten } from '../../utils' +import { TooltipChild } from '../TooltipChild' interface UserAvatarProps { - name?: string pubkey: string - image?: string + isNameVisible?: boolean } /** * This component will be used for the displaying username and profile picture. * Clicking will navigate to the user's profile. */ -export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => { +export const UserAvatar = ({ + pubkey, + isNameVisible = false +}: UserAvatarProps) => { + const profile = useProfileMetadata(pubkey) + const name = profile?.display_name || profile?.name || shorten(pubkey) + const image = profile?.picture + return ( - - {name ? {name} : null} + + + + + + {isNameVisible && name ? ( + {name} + ) : null} ) } diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx index bddae82..bfb4d11 100644 --- a/src/components/UsersDetails.tsx/index.tsx +++ b/src/components/UsersDetails.tsx/index.tsx @@ -1,11 +1,9 @@ import { Divider, Tooltip } from '@mui/material' -import { useSigitProfiles } from '../../hooks/useSigitProfiles' import { formatTimestamp, fromUnixTimestamp, hexToNpub, npubToHex, - shorten, SignStatus } from '../../utils' import { useSigitMeta } from '../../hooks/useSigitMeta' @@ -24,10 +22,10 @@ import { import { getExtensionIconLabel } from '../getExtensionIconLabel' import { useSelector } from 'react-redux' import { State } from '../../store/rootReducer' -import { TooltipChild } from '../TooltipChild' import { DisplaySigner } from '../DisplaySigner' import { Meta } from '../../types' import { extractFileExtensions } from '../../utils/file' +import { UserAvatar } from '../UserAvatar' interface UsersDetailsProps { meta: Meta @@ -36,6 +34,7 @@ interface UsersDetailsProps { export const UsersDetails = ({ meta }: UsersDetailsProps) => { const { submittedBy, + exportedBy, signers, viewers, fileHashes, @@ -47,11 +46,6 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { isValid } = useSigitMeta(meta) const { usersPubkey } = useSelector((state: State) => state.auth) - const profiles = useSigitProfiles([ - ...(submittedBy ? [submittedBy] : []), - ...signers, - ...viewers - ]) const userCanSign = typeof usersPubkey !== 'undefined' && signers.includes(hexToNpub(usersPubkey)) @@ -63,31 +57,12 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {

Signers

- {submittedBy && - (function () { - const profile = profiles[submittedBy] - return ( - - - - - - ) - })()} + {submittedBy && ( + + )} {submittedBy && signers.length ? ( @@ -96,26 +71,8 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { {signers.map((signer) => { const pubkey = npubToHex(signer)! - const profile = profiles[pubkey] - return ( - - - - - + ) })} @@ -128,34 +85,24 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { {viewers.map((signer) => { const pubkey = npubToHex(signer)! - const profile = profiles[pubkey] return ( - - - - - + ) })}
)} + + {exportedBy && ( + <> +

Exported By

+
+ +
+ + )}

Details

diff --git a/src/hooks/useProfileMetadata.tsx b/src/hooks/useProfileMetadata.tsx new file mode 100644 index 0000000..f746f0d --- /dev/null +++ b/src/hooks/useProfileMetadata.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react' +import { ProfileMetadata } from '../types/profile' +import { MetadataController } from '../controllers/MetadataController' +import { Event, kinds } from 'nostr-tools' + +export const useProfileMetadata = (pubkey: string) => { + const [profileMetadata, setProfileMetadata] = useState() + + useEffect(() => { + const metadataController = MetadataController.getInstance() + const handleMetadataEvent = (event: Event) => { + const metadataContent = + metadataController.extractProfileMetadataContent(event) + + if (metadataContent) { + setProfileMetadata(metadataContent) + } + } + + if (pubkey) { + metadataController.on(pubkey, (kind: number, event: Event) => { + if (kind === kinds.Metadata) { + handleMetadataEvent(event) + } + }) + + metadataController + .findMetadata(pubkey) + .then((metadataEvent) => { + if (metadataEvent) handleMetadataEvent(metadataEvent) + }) + .catch((err) => { + console.error( + `error occurred in finding metadata for: ${pubkey}`, + err + ) + }) + } + + return () => { + metadataController.off(pubkey, handleMetadataEvent) + } + }, [pubkey]) + + return profileMetadata +} diff --git a/src/hooks/useSigitProfiles.tsx b/src/hooks/useSigitProfiles.tsx deleted file mode 100644 index 88d6c50..0000000 --- a/src/hooks/useSigitProfiles.tsx +++ /dev/null @@ -1,71 +0,0 @@ -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)) - }) - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pubkeys]) - - return profileMetadata -} diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index f0bd6b9..2218620 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -36,7 +36,6 @@ import { npubToHex, queryNip05, sendNotification, - shorten, signEventForMetaFile, updateUsersAppData, uploadToFileStorage @@ -113,6 +112,8 @@ export const CreatePage = () => { const [error, setError] = useState() const [users, setUsers] = useState([]) + const signers = users.filter((u) => u.role === UserRole.signer) + const viewers = users.filter((u) => u.role === UserRole.viewer) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) @@ -252,7 +253,7 @@ export const CreatePage = () => { useEffect(() => { users.forEach((user) => { if (!(user.pubkey in metadata)) { - const metadataController = new MetadataController() + const metadataController = MetadataController.getInstance() const handleMetadataEvent = (event: Event) => { const metadataContent = @@ -647,6 +648,11 @@ export const CreatePage = () => { } saveAs(finalZipFile, `request-${unixNow()}.sigit.zip`) + + // If user is the next signer, we can navigate directly to sign page + if (signers[0].pubkey === usersPubkey) { + navigate(appPrivateRoutes.sign, { state: { uploadedZip: finalZipFile } }) + } setIsLoading(false) } @@ -672,9 +678,6 @@ export const CreatePage = () => { }, zipUrl: string ) => { - const signers = users.filter((user) => user.role === UserRole.signer) - const viewers = users.filter((user) => user.role === UserRole.viewer) - const content: CreateSignatureEventContent = { signers: signers.map((signer) => hexToNpub(signer.pubkey)), viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)), @@ -703,9 +706,6 @@ export const CreatePage = () => { // Send notifications to signers and viewers const sendNotifications = (meta: Meta) => { - const signers = users.filter((user) => user.role === UserRole.signer) - const viewers = users.filter((user) => user.role === UserRole.viewer) - // no need to send notification to self so remove it from the list const receivers = ( signers.length > 0 @@ -787,7 +787,7 @@ export const CreatePage = () => { toast.error('Failed to publish notifications') }) - navigate(appPrivateRoutes.sign, { state: { meta: meta } }) + navigate(appPrivateRoutes.sign, { state: { meta } }) } else { const zip = new JSZip() @@ -914,7 +914,6 @@ export const CreatePage = () => {
{ } type DisplayUsersProps = { - metadata: { [key: string]: ProfileMetadata } users: User[] handleUserRoleChange: (role: UserRole, pubkey: string) => void handleRemoveUser: (pubkey: string) => void @@ -1018,7 +1016,6 @@ type DisplayUsersProps = { } const DisplayUser = ({ - metadata, users, handleUserRoleChange, handleRemoveUser, @@ -1032,7 +1029,6 @@ const DisplayUser = ({ .map((user, index) => ( void handleRemoveUser: (pubkey: string) => void @@ -1078,7 +1072,6 @@ type SignerCounterpartProps = CounterpartProps & { } const SignerCounterpart = ({ - userMeta, user, index, moveSigner, @@ -1171,7 +1164,6 @@ const SignerCounterpart = ({ @@ -1180,7 +1172,6 @@ const SignerCounterpart = ({ } const Counterpart = ({ - userMeta, user, handleUserRoleChange, handleRemoveUser @@ -1188,15 +1179,7 @@ const Counterpart = ({ return ( <>
- +