diff --git a/src/components/LoadingSpinner/index.tsx b/src/components/LoadingSpinner/index.tsx index 2c6f4e5..d19d01f 100644 --- a/src/components/LoadingSpinner/index.tsx +++ b/src/components/LoadingSpinner/index.tsx @@ -1,12 +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': @@ -20,16 +22,22 @@ export const LoadingSpinner = (props: Props) => { ) default: - return ( + return createPortal(
- {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 { diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index adc3d1c..f4dc550 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -957,9 +957,6 @@ export const CreatePage = () => { -
{toolbox.map((drawTool: DrawTool, index: number) => { @@ -986,6 +983,10 @@ export const CreatePage = () => { })}
+ + {!!error && ( {error} )} diff --git a/src/pages/nostr/index.tsx b/src/pages/nostr/index.tsx index a9948af..5d9467f 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 = 5 +const EXTENSION_LOGIN_TIMEOUT_SECONDS = EXTENSION_LOGIN_DELAY_SECONDS + 55 + 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,43 @@ export const Nostr = () => { } const loginWithExtension = async () => { - setIsLoading(true) - setLoadingSpinnerDesc('Capturing pubkey from nostr extension') + let waitTimeout: number | undefined + try { + // Wait EXTENSION_LOGIN_DELAY_SECONDS before showing extension delay message + waitTimeout = window.setTimeout(() => { + setIsExtensionSlow(true) + }, EXTENSION_LOGIN_DELAY_SECONDS * 1000) - 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) { + navigateAfterLogin(redirectPath) + } + } catch (error) { + if (error instanceof TimeoutError) { + // 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) + } + } finally { + // Clear the wait timeout so we don't change the state unnecessarily + window.clearTimeout(waitTimeout) + + setIsLoading(false) + setLoadingSpinnerDesc('') + setIsExtensionSlow(false) + } } /** @@ -354,7 +375,33 @@ export const Nostr = () => { return ( <> - {isLoading && } + {isLoading && ( + + {isExtensionSlow && ( + <> +

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

+
+ + + )} +
+ )} {isNostrExtensionAvailable && ( <> 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..11053b9 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' @@ -34,7 +35,7 @@ export const isOnline = async () => { try { // Define a URL to check the online status - const url = 'https://www.google.com' + 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 @@ -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 }) }