diff --git a/src/api/hooks/nostr/useSignerStream.ts b/src/api/hooks/nostr/useSignerStream.ts index df221d175..35f4090d7 100644 --- a/src/api/hooks/nostr/useSignerStream.ts +++ b/src/api/hooks/nostr/useSignerStream.ts @@ -8,7 +8,10 @@ const secretStorageKey = 'soapbox:nip46:secret'; sessionStorage.setItem(secretStorageKey, crypto.randomUUID()); function useSignerStream() { - const { relay, signer } = useNostr(); + const [isSubscribed, setIsSubscribed] = useState(false); + const [isSubscribing, setIsSubscribing] = useState(true); + + const { relay, signer, hasNostr } = useNostr(); const [pubkey, setPubkey] = useState(undefined); const authStorageKey = `soapbox:nostr:auth:${pubkey}`; @@ -16,7 +19,7 @@ function useSignerStream() { useEffect(() => { let isCancelled = false; - if (signer) { + if (signer && hasNostr) { signer.getPublicKey().then((newPubkey) => { if (!isCancelled) { setPubkey(newPubkey); @@ -27,7 +30,7 @@ function useSignerStream() { return () => { isCancelled = true; }; - }, [signer]); + }, [signer, hasNostr]); useEffect(() => { if (!relay || !signer || !pubkey) return; @@ -39,6 +42,10 @@ function useSignerStream() { localStorage.setItem(authStorageKey, authorizedPubkey); sessionStorage.setItem(secretStorageKey, crypto.randomUUID()); }, + onSubscribed() { + setIsSubscribed(true); + setIsSubscribing(false); + }, authorizedPubkey: localStorage.getItem(authStorageKey) ?? undefined, getSecret: () => sessionStorage.getItem(secretStorageKey)!, }); @@ -47,6 +54,11 @@ function useSignerStream() { connect.close(); }; }, [relay, signer, pubkey]); + + return { + isSubscribed, + isSubscribing, + }; } export { useSignerStream }; diff --git a/src/contexts/nostr-context.tsx b/src/contexts/nostr-context.tsx index f138a1754..5eab5d15e 100644 --- a/src/contexts/nostr-context.tsx +++ b/src/contexts/nostr-context.tsx @@ -8,6 +8,8 @@ import { useInstance } from 'soapbox/hooks/useInstance'; interface NostrContextType { relay?: NRelay; signer?: NostrSigner; + hasNostr: boolean; + isRelayOpen: boolean; } const NostrContext = createContext(undefined); @@ -18,7 +20,10 @@ interface NostrProviderProps { export const NostrProvider: React.FC = ({ children }) => { const instance = useInstance(); + const hasNostr = !!instance.nostr; + const [relay, setRelay] = useState(); + const [isRelayOpen, setIsRelayOpen] = useState(false); const { account } = useOwnAccount(); @@ -30,17 +35,24 @@ export const NostrProvider: React.FC = ({ children }) => { [accountPubkey], ); + const handleRelayOpen = () => { + setIsRelayOpen(true); + }; + useEffect(() => { if (url) { - setRelay(new NRelay1(url)); + const relay = new NRelay1(url); + relay.socket.underlyingWebsocket.addEventListener('open', handleRelayOpen); + setRelay(relay); } return () => { + relay?.socket.underlyingWebsocket.removeEventListener('open', handleRelayOpen); relay?.close(); }; }, [url]); return ( - + {children} ); diff --git a/src/features/nostr/NConnect.ts b/src/features/nostr/NConnect.ts index 1eb23c7de..a87489b3b 100644 --- a/src/features/nostr/NConnect.ts +++ b/src/features/nostr/NConnect.ts @@ -5,6 +5,7 @@ interface NConnectOpts { signer: NostrSigner; authorizedPubkey: string | undefined; onAuthorize(pubkey: string): void; + onSubscribed(): void; getSecret(): string; } @@ -14,6 +15,7 @@ export class NConnect { private signer: NostrSigner; private authorizedPubkey: string | undefined; private onAuthorize: (pubkey: string) => void; + private onSubscribed: () => void; private getSecret: () => string; private controller = new AbortController(); @@ -23,6 +25,7 @@ export class NConnect { this.signer = opts.signer; this.authorizedPubkey = opts.authorizedPubkey; this.onAuthorize = opts.onAuthorize; + this.onSubscribed = opts.onSubscribed; this.getSecret = opts.getSecret; this.open(); @@ -32,7 +35,10 @@ export class NConnect { const pubkey = await this.signer.getPublicKey(); const signal = this.controller.signal; - for await (const msg of this.relay.req([{ kinds: [24133], '#p': [pubkey] }], { signal })) { + const sub = this.relay.req([{ kinds: [24133], '#p': [pubkey] }], { signal }); + this.onSubscribed(); + + for await (const msg of sub) { if (msg[0] === 'EVENT') { const event = msg[2]; this.handleEvent(event); diff --git a/src/init/soapbox-load.tsx b/src/init/soapbox-load.tsx index ccd3ce70e..2b1951863 100644 --- a/src/init/soapbox-load.tsx +++ b/src/init/soapbox-load.tsx @@ -6,6 +6,7 @@ import { fetchMe } from 'soapbox/actions/me'; import { loadSoapboxConfig } from 'soapbox/actions/soapbox'; import { useSignerStream } from 'soapbox/api/hooks/nostr/useSignerStream'; import LoadingScreen from 'soapbox/components/loading-screen'; +import { useNostr } from 'soapbox/contexts/nostr-context'; import { useAppSelector, useAppDispatch, @@ -44,7 +45,8 @@ const SoapboxLoad: React.FC = ({ children }) => { const [localeLoading, setLocaleLoading] = useState(true); const [isLoaded, setIsLoaded] = useState(false); - useSignerStream(); + const { hasNostr, isRelayOpen } = useNostr(); + const { isSubscribed } = useSignerStream(); /** Whether to display a loading indicator. */ const showLoading = [ @@ -53,6 +55,7 @@ const SoapboxLoad: React.FC = ({ children }) => { !isLoaded, localeLoading, swUpdating, + hasNostr && (!isRelayOpen || !isSubscribed), ].some(Boolean); // Load the user's locale