Hook up DVM request
This commit is contained in:
parent
338d0a7b3e
commit
37cc1e88f6
3 changed files with 163 additions and 3 deletions
59
src/features/nostr/hooks/useNostrReq.ts
Normal file
59
src/features/nostr/hooks/useNostrReq.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { NostrEvent, NostrFilter } from '@soapbox/nspec';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { useNostr } from 'soapbox/contexts/nostr-context';
|
||||||
|
|
||||||
|
/** Streams events from the relay for the given filters. */
|
||||||
|
export function useNostrReq(filters: NostrFilter[]): { events: NostrEvent[]; eose: boolean; closed: boolean } {
|
||||||
|
const { relay } = useNostr();
|
||||||
|
|
||||||
|
const [events, setEvents] = useState<NostrEvent[]>([]);
|
||||||
|
const [closed, setClosed] = useState(false);
|
||||||
|
const [eose, setEose] = useState(false);
|
||||||
|
|
||||||
|
const controller = useRef<AbortController>(new AbortController());
|
||||||
|
const signal = controller.current.signal;
|
||||||
|
const value = useValue(filters);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (relay && value.length) {
|
||||||
|
(async () => {
|
||||||
|
for await (const msg of relay.req(value, { signal })) {
|
||||||
|
if (msg[0] === 'EVENT') {
|
||||||
|
setEvents((prev) => [msg[2], ...prev]);
|
||||||
|
} else if (msg[0] === 'EOSE') {
|
||||||
|
setEose(true);
|
||||||
|
} else if (msg[0] === 'CLOSED') {
|
||||||
|
setClosed(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
controller.current.abort();
|
||||||
|
controller.current = new AbortController();
|
||||||
|
setEose(false);
|
||||||
|
setClosed(false);
|
||||||
|
};
|
||||||
|
}, [relay, value]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
eose,
|
||||||
|
closed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preserves the memory reference of a value across re-renders. */
|
||||||
|
function useValue<T>(value: T): T {
|
||||||
|
const ref = useRef<T>(value);
|
||||||
|
|
||||||
|
if (!isEqual(ref.current, value)) {
|
||||||
|
ref.current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.current;
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { NRelay, NostrEvent, NostrSigner } from '@soapbox/nspec';
|
||||||
|
|
||||||
|
interface DittoSignupRequestOpts {
|
||||||
|
dvm: string;
|
||||||
|
url: string;
|
||||||
|
relay: NRelay;
|
||||||
|
signer: NostrSigner;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DittoSignup {
|
||||||
|
|
||||||
|
static async request(opts: DittoSignupRequestOpts): Promise<NostrEvent> {
|
||||||
|
const { dvm, url, relay, signer, signal } = opts;
|
||||||
|
|
||||||
|
const pubkey = await signer.getPublicKey();
|
||||||
|
const event = await signer.signEvent({
|
||||||
|
kind: 5951,
|
||||||
|
content: '',
|
||||||
|
tags: [
|
||||||
|
['i', url, 'text'],
|
||||||
|
['p', dvm],
|
||||||
|
],
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
const subscription = relay.req(
|
||||||
|
[{ kinds: [7000, 6951], authors: [dvm], '#p': [pubkey], '#e': [event.id] }],
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
await relay.event(event, { signal });
|
||||||
|
|
||||||
|
for await (const msg of subscription) {
|
||||||
|
if (msg[0] === 'EVENT') {
|
||||||
|
return msg[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('DittoSignup: no response');
|
||||||
|
}
|
||||||
|
|
||||||
|
static async check(opts: Omit<DittoSignupRequestOpts, 'url'>): Promise<NostrEvent | undefined> {
|
||||||
|
const { dvm, relay, signer, signal } = opts;
|
||||||
|
|
||||||
|
const pubkey = await signer.getPublicKey();
|
||||||
|
const [event] = await relay.query(
|
||||||
|
[{ kinds: [7000, 6951], authors: [dvm], '#p': [pubkey] }],
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
import { NSchema as n } from '@soapbox/nspec';
|
import { NSchema as n } from '@soapbox/nspec';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { useAccount } from 'soapbox/api/hooks';
|
import { useAccount } from 'soapbox/api/hooks';
|
||||||
import { Avatar, Text, Stack, Emoji, Button, Tooltip, Modal } from 'soapbox/components/ui';
|
import { Avatar, Text, Stack, Emoji, Button, Tooltip, Modal } from 'soapbox/components/ui';
|
||||||
|
import { useNostr } from 'soapbox/contexts/nostr-context';
|
||||||
|
import { useNostrReq } from 'soapbox/features/nostr/hooks/useNostrReq';
|
||||||
import ModalLoading from 'soapbox/features/ui/components/modal-loading';
|
import ModalLoading from 'soapbox/features/ui/components/modal-loading';
|
||||||
import { useInstance } from 'soapbox/hooks';
|
import { useInstance } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
@ -16,9 +18,41 @@ interface IAccountStep {
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountStep: React.FC<IAccountStep> = ({ accountId, setStep, onClose }) => {
|
const AccountStep: React.FC<IAccountStep> = ({ accountId, setStep, onClose }) => {
|
||||||
|
const { relay, signer } = useNostr();
|
||||||
const { account } = useAccount(accountId);
|
const { account } = useAccount(accountId);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [submitted, setSubmitted] = useState(false);
|
||||||
const instance = useInstance();
|
const instance = useInstance();
|
||||||
|
|
||||||
|
const { events } = useNostrReq((instance.nostr && account?.nostr) ? [{
|
||||||
|
kinds: [7000, 6951],
|
||||||
|
authors: [instance.nostr.pubkey],
|
||||||
|
'#p': [account.nostr.pubkey],
|
||||||
|
}] : []);
|
||||||
|
|
||||||
|
const success = events.find((event) => event.kind === 6951);
|
||||||
|
const feedback = events.find((event) => event.kind === 7000);
|
||||||
|
|
||||||
|
const handleJoin = async () => {
|
||||||
|
if (!relay || !signer || !instance.nostr) return;
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
const event = await signer.signEvent({
|
||||||
|
kind: 5951,
|
||||||
|
content: '',
|
||||||
|
tags: [
|
||||||
|
['i', instance.nostr.relay, 'text'],
|
||||||
|
['p', instance.nostr.pubkey, 'text'],
|
||||||
|
],
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
await relay.event(event);
|
||||||
|
|
||||||
|
setSubmitting(false);
|
||||||
|
setSubmitted(true);
|
||||||
|
};
|
||||||
|
|
||||||
const username = useMemo(
|
const username = useMemo(
|
||||||
() => n.bech32().safeParse(account?.acct).success ? account?.acct.slice(0, 13) : account?.acct,
|
() => n.bech32().safeParse(account?.acct).success ? account?.acct.slice(0, 13) : account?.acct,
|
||||||
[account?.acct],
|
[account?.acct],
|
||||||
|
@ -63,11 +97,23 @@ const AccountStep: React.FC<IAccountStep> = ({ accountId, setStep, onClose }) =>
|
||||||
<Emoji className='h-16 w-16' emoji='🫂' />
|
<Emoji className='h-16 w-16' emoji='🫂' />
|
||||||
|
|
||||||
<Text align='center' className='max-w-72'>
|
<Text align='center' className='max-w-72'>
|
||||||
You need an account on {instance.title} to continue.
|
{(success || feedback) ? (
|
||||||
|
JSON.stringify(success || feedback, null, 2)
|
||||||
|
) : (
|
||||||
|
<>You need an account on {instance.title} to continue.</>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Button theme='accent' size='lg' block>Join</Button>
|
<Button
|
||||||
|
theme='accent'
|
||||||
|
size='lg'
|
||||||
|
onClick={handleJoin}
|
||||||
|
disabled={submitting || submitted}
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Join
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
Loading…
Reference in a new issue