Compare commits
14 Commits
68c10d1831
...
84c374bb2c
Author | SHA1 | Date | |
---|---|---|---|
84c374bb2c | |||
a53914b59d | |||
5a2a0ad9c4 | |||
8b4f1a8973 | |||
79ef9eb8d6 | |||
ba24e7417d | |||
ea7e3a0964 | |||
2e58b58a6a | |||
17c1700554 | |||
9191336722 | |||
7c80643aba | |||
9c545a477c | |||
4d1e672268 | |||
4bc5882ab6 |
@ -1,12 +1,14 @@
|
|||||||
|
import { createPortal } from 'react-dom'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
desc?: string
|
desc?: string
|
||||||
variant?: 'small' | 'default'
|
variant?: 'small' | 'default'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoadingSpinner = (props: Props) => {
|
export const LoadingSpinner = (props: PropsWithChildren<Props>) => {
|
||||||
const { desc, variant = 'default' } = props
|
const { desc, children, variant = 'default' } = props
|
||||||
|
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'small':
|
case 'small':
|
||||||
@ -20,16 +22,22 @@ export const LoadingSpinner = (props: Props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (
|
return createPortal(
|
||||||
<div className={styles.loadingSpinnerOverlay}>
|
<div className={styles.loadingSpinnerOverlay}>
|
||||||
<div
|
<div
|
||||||
className={styles.loadingSpinnerContainer}
|
className={styles.loadingSpinnerContainer}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
>
|
>
|
||||||
<div className={styles.loadingSpinner}></div>
|
<div className={styles.loadingSpinner}></div>
|
||||||
{desc && <p className={styles.loadingSpinnerDesc}>{desc}</p>}
|
{desc && (
|
||||||
|
<div className={styles.loadingSpinnerDesc}>
|
||||||
|
{desc}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.getElementById('root')!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,11 +42,15 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-top: solid 1px rgba(0, 0, 0, 0.1);
|
border-top: solid 1px rgba(0, 0, 0, 0.1);
|
||||||
text-align: center;
|
|
||||||
color: rgba(0, 0, 0, 0.5);
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
|
@ -957,9 +957,6 @@ export const CreatePage = () => {
|
|||||||
<FontAwesomeIcon icon={faPlus} />
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleCreate} variant="contained">
|
|
||||||
Publish
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className={`${styles.paperGroup} ${styles.toolbox}`}>
|
<div className={`${styles.paperGroup} ${styles.toolbox}`}>
|
||||||
{toolbox.map((drawTool: DrawTool, index: number) => {
|
{toolbox.map((drawTool: DrawTool, index: number) => {
|
||||||
@ -986,6 +983,10 @@ export const CreatePage = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={handleCreate} variant="contained">
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
|
||||||
{!!error && (
|
{!!error && (
|
||||||
<FormHelperText error={!!error}>{error}</FormHelperText>
|
<FormHelperText error={!!error}>{error}</FormHelperText>
|
||||||
)}
|
)}
|
||||||
|
@ -18,12 +18,16 @@ import {
|
|||||||
} from '../../store/actions'
|
} from '../../store/actions'
|
||||||
import { LoginMethods } from '../../store/auth/types'
|
import { LoginMethods } from '../../store/auth/types'
|
||||||
import { Dispatch } from '../../store/store'
|
import { Dispatch } from '../../store/store'
|
||||||
import { npubToHex, queryNip05 } from '../../utils'
|
import { npubToHex, queryNip05, timeout } from '../../utils'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
import { NIP05_REGEX } from '../../constants'
|
import { NIP05_REGEX } from '../../constants'
|
||||||
|
|
||||||
import styles from './styles.module.scss'
|
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 = () => {
|
export const Nostr = () => {
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
@ -36,6 +40,7 @@ export const Nostr = () => {
|
|||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
|
const [isExtensionSlow, setIsExtensionSlow] = useState(false)
|
||||||
const [inputValue, setInputValue] = useState('')
|
const [inputValue, setInputValue] = useState('')
|
||||||
const [authUrl, setAuthUrl] = useState<string>()
|
const [authUrl, setAuthUrl] = useState<string>()
|
||||||
|
|
||||||
@ -72,27 +77,43 @@ export const Nostr = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loginWithExtension = async () => {
|
const loginWithExtension = async () => {
|
||||||
setIsLoading(true)
|
let waitTimeout: number | undefined
|
||||||
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
try {
|
||||||
|
// Wait EXTENSION_LOGIN_DELAY_SECONDS before showing extension delay message
|
||||||
|
waitTimeout = window.setTimeout(() => {
|
||||||
|
setIsExtensionSlow(true)
|
||||||
|
}, EXTENSION_LOGIN_DELAY_SECONDS * 1000)
|
||||||
|
|
||||||
nostrController
|
setIsLoading(true)
|
||||||
.capturePublicKey()
|
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
||||||
.then(async (pubkey) => {
|
|
||||||
dispatch(updateLoginMethod(LoginMethods.extension))
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
const pubkey = await nostrController.capturePublicKey()
|
||||||
const redirectPath =
|
dispatch(updateLoginMethod(LoginMethods.extension))
|
||||||
await authController.authAndGetMetadataAndRelaysMap(pubkey)
|
|
||||||
|
|
||||||
if (redirectPath) navigateAfterLogin(redirectPath)
|
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||||
})
|
const redirectPath = await Promise.race([
|
||||||
.catch((err) => {
|
authController.authAndGetMetadataAndRelaysMap(pubkey),
|
||||||
toast.error('Error capturing public key from nostr extension: ' + err)
|
timeout(EXTENSION_LOGIN_TIMEOUT_SECONDS * 1000)
|
||||||
})
|
])
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
if (redirectPath) {
|
||||||
setLoadingSpinnerDesc('')
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
{isLoading && (
|
||||||
|
<LoadingSpinner desc={loadingSpinnerDesc}>
|
||||||
|
{isExtensionSlow && (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Your nostr extension is not responding. Check these
|
||||||
|
alternatives:{' '}
|
||||||
|
<a href="https://github.com/aljazceru/awesome-nostr?tab=readme-ov-file#nip-07-browser-extensions">
|
||||||
|
https://github.com/aljazceru/awesome-nostr
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
setLoadingSpinnerDesc('')
|
||||||
|
setIsLoading(false)
|
||||||
|
setIsExtensionSlow(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</LoadingSpinner>
|
||||||
|
)}
|
||||||
|
|
||||||
{isNostrExtensionAvailable && (
|
{isNostrExtensionAvailable && (
|
||||||
<>
|
<>
|
||||||
|
6
src/types/errors/TimeoutError.ts
Normal file
6
src/types/errors/TimeoutError.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export class TimeoutError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Timeout')
|
||||||
|
this.name = this.constructor.name
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { TimeoutError } from '../types/errors/TimeoutError.ts'
|
||||||
import { CurrentUserFile } from '../types/file.ts'
|
import { CurrentUserFile } from '../types/file.ts'
|
||||||
import { SigitFile } from './file.ts'
|
import { SigitFile } from './file.ts'
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ export const isOnline = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Define a URL to check the online status
|
// 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
|
// 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
|
// 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
|
// Set a timeout using setTimeout
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Reject the promise with an Error indicating a timeout
|
// Reject the promise with an Error indicating a timeout
|
||||||
reject(new Error('Timeout'))
|
reject(new TimeoutError())
|
||||||
}, ms) // Timeout duration in milliseconds
|
}, ms) // Timeout duration in milliseconds
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user