Hook up DVM request

This commit is contained in:
Alex Gleason 2024-03-12 13:41:01 -05:00
parent 338d0a7b3e
commit 37cc1e88f6
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
3 changed files with 163 additions and 3 deletions

View 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;
}

View file

@ -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;
}
}

View file

@ -1,9 +1,11 @@
import { NSchema as n } from '@soapbox/nspec';
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useAccount } from 'soapbox/api/hooks';
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 { useInstance } from 'soapbox/hooks';
@ -16,9 +18,41 @@ interface IAccountStep {
}
const AccountStep: React.FC<IAccountStep> = ({ accountId, setStep, onClose }) => {
const { relay, signer } = useNostr();
const { account } = useAccount(accountId);
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
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(
() => n.bech32().safeParse(account?.acct).success ? account?.acct.slice(0, 13) : account?.acct,
[account?.acct],
@ -63,11 +97,23 @@ const AccountStep: React.FC<IAccountStep> = ({ accountId, setStep, onClose }) =>
<Emoji className='h-16 w-16' emoji='🫂' />
<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>
</Stack>
<Button theme='accent' size='lg' block>Join</Button>
<Button
theme='accent'
size='lg'
onClick={handleJoin}
disabled={submitting || submitted}
block
>
Join
</Button>
</Stack>
)}
</Stack>