rebuild translation modal to support multiple DVMs

This commit is contained in:
hzrd149
2023-11-18 10:24:20 -06:00
parent 7ad9dc1dec
commit 99537864bd

View File

@@ -1,5 +1,11 @@
import { useCallback, useState } from "react"; import { MouseEventHandler, useCallback, useState } from "react";
import { import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Button, Button,
Card, Card,
CardBody, CardBody,
@@ -14,6 +20,7 @@ import {
ModalOverlay, ModalOverlay,
ModalProps, ModalProps,
Select, Select,
Spacer,
Spinner, Spinner,
Text, Text,
useToast, useToast,
@@ -43,65 +50,53 @@ function getTranslationRequestLanguage(request: NostrEvent) {
return codes.find((code) => code.iso639_1 === targetLanguage); return codes.find((code) => code.iso639_1 === targetLanguage);
} }
function TranslationResult({ result, request }: { result: NostrEvent; request?: NostrEvent }) {
const requester = result.tags.find(isPTag)?.[1];
const lang = request && getTranslationRequestLanguage(request);
return (
<Card variant="outline">
<CardHeader px="4" py="4" pb="2" display="flex" gap="2" alignItems="center">
<UserAvatarLink pubkey={result.pubkey} size="sm" />
<UserLink pubkey={result.pubkey} fontWeight="bold" />
{lang && <Text>Translated to {lang.nativeName}</Text>}
<Timestamp timestamp={result.created_at} />
</CardHeader>
<CardBody px="4" pt="0" pb="4">
{requester && (
<Text fontStyle="italic" mb="2">
Requested by <UserLink pubkey={requester} fontWeight="bold" />
</Text>
)}
<NoteContents event={result} />
</CardBody>
</Card>
);
}
function TranslationRequest({ request }: { request: NostrEvent }) { function TranslationRequest({ request }: { request: NostrEvent }) {
const lang = getTranslationRequestLanguage(request); const lang = getTranslationRequestLanguage(request);
const requestRelays = request.tags.find((t) => t[0] === "relays")?.slice(1); const requestRelays = request.tags.find((t) => t[0] === "relays")?.slice(1);
const readRelays = useReadRelayUrls(); const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader(`${getEventUID(request)}-offers`, requestRelays || readRelays, { const timeline = useTimelineLoader(`${getEventUID(request)}-offers-results`, requestRelays || readRelays, {
kinds: [DMV_STATUS_KIND], kinds: [DMV_STATUS_KIND, DMV_TRANSLATE_RESULT_KIND],
"#e": [request.id], "#e": [request.id],
}); });
const offers = useSubject(timeline.timeline); const events = useSubject(timeline.timeline);
const dvmStatuses: Record<string, NostrEvent> = {};
for (const event of events) {
if (
(event.kind === DMV_STATUS_KIND || event.kind === DMV_TRANSLATE_RESULT_KIND) &&
(!dvmStatuses[event.pubkey] || dvmStatuses[event.pubkey].created_at < event.created_at)
) {
dvmStatuses[event.pubkey] = event;
}
}
return ( return (
<Card variant="outline"> <Card variant="outline">
<CardHeader px="4" py="4" pb="2" display="flex" gap="2" alignItems="center" flexWrap="wrap"> <CardHeader px="4" py="4" pb="2" display="flex" gap="2" alignItems="center" flexWrap="wrap">
<UserAvatarLink pubkey={request.pubkey} size="sm" /> <UserAvatarLink pubkey={request.pubkey} size="sm" />
<UserLink pubkey={request.pubkey} fontWeight="bold" /> <UserLink pubkey={request.pubkey} fontWeight="bold" />
<Text>Requested translation to {lang?.nativeName}</Text> <Text>
Requested translation to <strong>{lang?.nativeName}</strong>
</Text>
<Timestamp timestamp={request.created_at} /> <Timestamp timestamp={request.created_at} />
</CardHeader> </CardHeader>
<CardBody px="4" pt="0" pb="4"> {Object.keys(dvmStatuses).length === 0 && (
{offers.length === 0 ? ( <Flex gap="2" alignItems="center" m="4">
<Flex gap="2" alignItems="center"> <Spinner />
<Spinner /> Waiting for offers
Waiting for offers </Flex>
</Flex> )}
) : ( <Accordion allowMultiple>
<Heading size="md" mb="2"> {Object.values(dvmStatuses).map((event) => {
Offers ({offers.length}) switch (event.kind) {
</Heading> case DMV_STATUS_KIND:
)} return <TranslationOffer key={event.id} offer={event} />;
{offers.map((offer) => ( case DMV_TRANSLATE_RESULT_KIND:
<TranslationOffer key={offer.id} offer={offer} /> return <TranslationResult key={event.id} result={event} />;
))} }
</CardBody> })}
</Accordion>
</Card> </Card>
); );
} }
@@ -115,10 +110,11 @@ function TranslationOffer({ offer }: { offer: NostrEvent }) {
const [paid, setPaid] = useState(false); const [paid, setPaid] = useState(false);
const [paying, setPaying] = useState(false); const [paying, setPaying] = useState(false);
const payInvoice = async () => { const payInvoice: MouseEventHandler = async (e) => {
try { try {
if (window.webln && invoice) { if (window.webln && invoice) {
setPaying(true); setPaying(true);
e.stopPropagation();
await window.webln.sendPayment(invoice); await window.webln.sendPayment(invoice);
setPaid(true); setPaid(true);
} }
@@ -129,27 +125,51 @@ function TranslationOffer({ offer }: { offer: NostrEvent }) {
}; };
return ( return (
<Flex gap="2" direction="column"> <AccordionItem>
<Flex gap="2" alignItems="center"> <AccordionButton>
<UserAvatarLink pubkey={offer.pubkey} size="sm" /> <Flex gap="2" alignItems="center" grow={1}>
<UserLink pubkey={offer.pubkey} fontWeight="bold" /> <UserAvatarLink pubkey={offer.pubkey} size="sm" />
<UserLink pubkey={offer.pubkey} fontWeight="bold" />
<Text>Offered</Text>
<Spacer />
{invoice && amountMsat && ( {invoice && amountMsat && (
<Button <Button
colorScheme="yellow" colorScheme="yellow"
ml="auto" size="sm"
size="sm" leftIcon={<LightningIcon />}
leftIcon={<LightningIcon />} onClick={payInvoice}
onClick={payInvoice} isLoading={paying || paid}
isLoading={paying || paid} isDisabled={!window.webln}
isDisabled={!window.webln} >
> Pay {readablizeSats(amountMsat / 1000)} sats
Pay {readablizeSats(amountMsat / 1000)} sats </Button>
</Button> )}
)} <AccordionIcon />
</Flex> </Flex>
<Text>{offer.content}</Text> </AccordionButton>
</Flex> <AccordionPanel pb={4}>
<Text>{offer.content}</Text>
</AccordionPanel>
</AccordionItem>
);
}
function TranslationResult({ result }: { result: NostrEvent }) {
return (
<AccordionItem>
<AccordionButton>
<Flex gap="2" alignItems="center" grow={1}>
<UserAvatarLink pubkey={result.pubkey} size="sm" />
<UserLink pubkey={result.pubkey} fontWeight="bold" />
<Text>Translated Note</Text>
<AccordionIcon ml="auto" />
</Flex>
</AccordionButton>
<AccordionPanel pb={4}>
<NoteContents event={result} />
</AccordionPanel>
</AccordionItem>
); );
} }
@@ -187,16 +207,12 @@ export default function NoteTranslationModal({
}, [requestSignature, note, readRelays]); }, [requestSignature, note, readRelays]);
const timeline = useTimelineLoader(`${getEventUID(note)}-translations`, readRelays, { const timeline = useTimelineLoader(`${getEventUID(note)}-translations`, readRelays, {
kinds: [DMV_TRANSLATE_JOB_KIND, DMV_TRANSLATE_RESULT_KIND], kinds: [DMV_TRANSLATE_JOB_KIND],
"#i": [note.id], "#i": [note.id],
}); });
const events = useSubject(timeline.timeline); const events = useSubject(timeline.timeline);
const filteredEvents = events.filter( const jobs = events.filter((e) => e.kind === DMV_TRANSLATE_JOB_KIND);
(e, i, arr) =>
e.kind === DMV_TRANSLATE_RESULT_KIND ||
(e.kind === DMV_TRANSLATE_JOB_KIND && !arr.some((r) => r.tags.some((t) => isETag(t) && t[1] === e.id))),
);
return ( return (
<Modal isOpen={isOpen} onClose={onClose} size="4xl" {...props}> <Modal isOpen={isOpen} onClose={onClose} size="4xl" {...props}>
@@ -217,16 +233,9 @@ export default function NoteTranslationModal({
Request new translation Request new translation
</Button> </Button>
</Flex> </Flex>
{filteredEvents.map((event) => { {jobs.map((event) => (
switch (event.kind) { <TranslationRequest key={event.id} request={event} />
case DMV_TRANSLATE_JOB_KIND: ))}
return <TranslationRequest key={event.id} request={event} />;
case DMV_TRANSLATE_RESULT_KIND:
const requestId = event.tags.find(isETag)?.[1];
const request = events.find((e) => e.id === requestId);
return <TranslationResult key={event.id} result={event} request={request} />;
}
})}
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
</Modal> </Modal>