mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-05 00:51:14 +02:00
rebuild translation modal to support multiple DVMs
This commit is contained in:
@@ -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>
|
||||||
|
Reference in New Issue
Block a user