show more relay info

This commit is contained in:
hzrd149 2023-08-05 10:53:43 -05:00
parent 33da3e2fa9
commit a60408220e
5 changed files with 345 additions and 159 deletions

View File

@ -33,7 +33,7 @@ async function getInfo(relay: string) {
const cached = await db.get("relayInfo", relay);
if (cached) {
memoryCache.set(relay, cached);
return cached;
return cached as RelayInformationDocument;
}
return fetchInfo(relay);

View File

@ -0,0 +1,105 @@
import {
Box,
Button,
ButtonGroup,
Code,
Flex,
FormControl,
FormLabel,
IconButton,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
ModalProps,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { useState } from "react";
import { useRelayInfo } from "../../hooks/use-relay-info";
import { UserAvatar } from "../../components/user-avatar";
import { UserLink } from "../../components/user-link";
import { safeRelayUrl } from "../../helpers/url";
import { useDebounce } from "react-use";
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
import { CodeIcon } from "../../components/icons";
import { Metadata } from "./relay-card";
function RelayDetails({ url, debug }: { url: string; debug?: boolean }) {
const { info } = useRelayInfo(url);
if (!info) return null;
return (
<Box>
<Metadata name="Name">{info.name}</Metadata>
<Metadata name="URL">{url}</Metadata>
{info.pubkey && (
<Flex gap="2" alignItems="center">
<Text fontWeight="bold">Owner: </Text>
<UserAvatar pubkey={info.pubkey} size="xs" />
<UserLink pubkey={info.pubkey} />
<UserDnsIdentityIcon pubkey={info.pubkey} onlyIcon />
</Flex>
)}
<Metadata name="NIPs">{info.supported_nips?.join(", ")}</Metadata>
{debug && (
<Code whiteSpace="pre" overflow="auto">
{JSON.stringify(info, null, 2)}
</Code>
)}
</Box>
);
}
export default function AddCustomRelayModal({
onSubmit,
...props
}: { onSubmit: (relay: string) => void } & Omit<ModalProps, "children">) {
const [url, setUrl] = useState("");
const [safeUrl, setSafeUrl] = useState<string>();
const showDebug = useDisclosure();
useDebounce(() => setSafeUrl(safeRelayUrl(url) ?? undefined), 1000, [url]);
return (
<Modal {...props}>
<ModalOverlay />
<ModalContent>
<ModalHeader p="4">Custom Relay</ModalHeader>
<ModalCloseButton />
<ModalBody px="4" py="0" display="flex" flexDirection="column" gap="2">
<FormControl>
<FormLabel>Relay URL</FormLabel>
<Input
type="url"
placeholder="wss://relay.example.com"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</FormControl>
{safeUrl && <RelayDetails url={safeUrl} debug={showDebug.isOpen} />}
</ModalBody>
<ModalFooter p="4">
<ButtonGroup size="sm">
{safeUrl && (
<IconButton icon={<CodeIcon />} aria-label="Show JSON" onClick={showDebug.onToggle} variant="ghost" />
)}
<Button variant="ghost" onClick={props.onClose}>
Cancel
</Button>
<Button colorScheme="brand" onClick={() => safeUrl && onSubmit(safeUrl)} isDisabled={!safeUrl}>
Add
</Button>
</ButtonGroup>
</ModalFooter>
</ModalContent>
</Modal>
);
}

View File

@ -1,170 +1,21 @@
import {
Box,
Button,
ButtonGroup,
Card,
CardBody,
CardHeader,
Divider,
Flex,
Heading,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
ModalProps,
SimpleGrid,
Spacer,
Switch,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { useAsync } from "react-use";
import { Link as RouterLink } from "react-router-dom";
import { useRelayInfo } from "../../hooks/use-relay-info";
import { RelayFavicon } from "../../components/relay-favicon";
import { useDeferredValue, useMemo, useState } from "react";
import { ExternalLinkIcon } from "../../components/icons";
import { UserLink } from "../../components/user-link";
import { UserAvatar } from "../../components/user-avatar";
import { useClientRelays, useReadRelayUrls } from "../../hooks/use-client-relays";
import { Button, Divider, Flex, Heading, Input, SimpleGrid, Spacer, Switch, useDisclosure } from "@chakra-ui/react";
import { useAsync } from "react-use";
import { useClientRelays } from "../../hooks/use-client-relays";
import relayPoolService from "../../services/relay-pool";
import { safeRelayUrl } from "../../helpers/url";
import AddCustomRelayModal from "./add-custom-modal";
import RelayCard from "./relay-card";
import clientRelaysService from "../../services/client-relays";
import { RelayMode } from "../../classes/relay";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
import { UserAvatarLink } from "../../components/user-avatar-link";
import { NostrEvent } from "../../types/nostr-event";
import dayjs from "dayjs";
import { safeJson } from "../../helpers/parse";
import StarRating from "../../components/star-rating";
import relayPoolService from "../../services/relay-pool";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { safeRelayUrl } from "../../helpers/url";
function RelayReviewNote({ event }: { event: NostrEvent }) {
const ratingJson = event.tags.find((t) => t[0] === "l" && t[3])?.[3];
const rating = ratingJson ? (safeJson(ratingJson, undefined) as { quality: number } | undefined) : undefined;
return (
<Card variant="outline">
<CardHeader display="flex" gap="2" px="2" pt="2" pb="0">
<UserAvatarLink pubkey={event.pubkey} size="xs" />
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
<Spacer />
<Text>{dayjs.unix(event.created_at).fromNow()}</Text>
</CardHeader>
<CardBody p="2" gap="2" display="flex" flexDirection="column">
{rating && <StarRating quality={rating.quality} color="yellow.400" />}
<Box whiteSpace="pre-wrap">{event.content}</Box>
</CardBody>
</Card>
);
}
function RelayReviewsModal({ relay, ...props }: { relay: string } & Omit<ModalProps, "children">) {
const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader(`${relay}-reviews`, readRelays, {
kinds: [1985],
"#r": [relay],
"#l": ["review/relay"],
});
const events = useSubject(timeline.timeline);
return (
<Modal {...props}>
<ModalOverlay />
<ModalContent>
<ModalHeader p="4" pb="0">
{relay} reviews
</ModalHeader>
<ModalCloseButton />
<ModalBody px="4" pt="0" pb="4">
<Flex gap="2" direction="column">
{events.map((event) => (
<RelayReviewNote key={event.id} event={event} />
))}
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
}
function RelayCard({ url }: { url: string }) {
const account = useCurrentAccount();
const { info } = useRelayInfo(url);
const clientRelays = useClientRelays();
const reviewsModal = useDisclosure();
const joined = clientRelays.some((r) => r.url === url);
return (
<>
<Card>
<CardHeader display="flex" gap="2" alignItems="center" p="2">
<RelayFavicon relay={url} size="xs" />
<Heading size="md" isTruncated>
{url}
</Heading>
</CardHeader>
<CardBody p="2" display="flex" flexDirection="column" gap="2">
{info?.pubkey && (
<Flex gap="2" alignItems="center">
<Text fontWeight="bold">Owner:</Text>
<UserAvatar pubkey={info.pubkey} size="xs" />
<UserLink pubkey={info.pubkey} />
<UserDnsIdentityIcon pubkey={info.pubkey} onlyIcon />
</Flex>
)}
<ButtonGroup size="sm">
{joined ? (
<Button
colorScheme="red"
variant="outline"
onClick={() => clientRelaysService.removeRelay(url)}
isDisabled={!account}
>
Leave
</Button>
) : (
<Button
colorScheme="green"
onClick={() => clientRelaysService.addRelay(url, RelayMode.ALL)}
isDisabled={!account}
>
Join
</Button>
)}
<Button onClick={reviewsModal.onOpen}>Reviews</Button>
<Button as={RouterLink} to={`/global?relay=${url}`}>
Notes
</Button>
<Button
as="a"
href={`https://nostr.watch/relay/${new URL(url).host}`}
target="_blank"
rightIcon={<ExternalLinkIcon />}
>
More info
</Button>
</ButtonGroup>
</CardBody>
</Card>
{reviewsModal.isOpen && <RelayReviewsModal isOpen onClose={reviewsModal.onClose} relay={url} size="2xl" />}
</>
);
}
export default function RelaysView() {
const [search, setSearch] = useState("");
const deboundedSearch = useDeferredValue(search);
const isSearching = deboundedSearch.length > 2;
const showAll = useDisclosure();
const addRelayModal = useDisclosure();
const clientRelays = useClientRelays().map((r) => r.url);
const discoveredRelays = relayPoolService
@ -191,6 +42,10 @@ export default function RelaysView() {
<Switch isChecked={showAll.isOpen} onChange={showAll.onToggle}>
Show All
</Switch>
<Spacer />
<Button colorScheme="brand" onClick={addRelayModal.onOpen}>
Add Custom
</Button>
</Flex>
<SimpleGrid minChildWidth="25rem" spacing="2">
{filteredRelays.map((url) => (
@ -209,6 +64,18 @@ export default function RelaysView() {
</SimpleGrid>
</>
)}
{addRelayModal.isOpen && (
<AddCustomRelayModal
isOpen
onClose={addRelayModal.onClose}
size="2xl"
onSubmit={(url) => {
clientRelaysService.addRelay(url, RelayMode.ALL);
addRelayModal.onClose();
}}
/>
)}
</Flex>
);
}

View File

@ -0,0 +1,184 @@
import {
Box,
Button,
ButtonGroup,
Card,
CardBody,
CardFooter,
CardHeader,
Code,
Flex,
Heading,
IconButton,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
ModalProps,
Spacer,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { useRelayInfo } from "../../hooks/use-relay-info";
import { RelayFavicon } from "../../components/relay-favicon";
import { CodeIcon, ExternalLinkIcon } from "../../components/icons";
import { UserLink } from "../../components/user-link";
import { UserAvatar } from "../../components/user-avatar";
import { useClientRelays, useReadRelayUrls } from "../../hooks/use-client-relays";
import clientRelaysService from "../../services/client-relays";
import { RelayMode } from "../../classes/relay";
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
import { useCurrentAccount } from "../../hooks/use-current-account";
import useSubject from "../../hooks/use-subject";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import RelayReviewNote from "./relay-review-note";
import styled from "@emotion/styled";
import { PropsWithChildren } from "react";
import RawJson from "../../components/debug-modals/raw-json";
function RelayReviewsModal({ relay, ...props }: { relay: string } & Omit<ModalProps, "children">) {
const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader(`${relay}-reviews`, readRelays, {
kinds: [1985],
"#r": [relay],
"#l": ["review/relay"],
});
const events = useSubject(timeline.timeline);
return (
<Modal {...props}>
<ModalOverlay />
<ModalContent>
<ModalHeader p="4" pb="0">
{relay} reviews
</ModalHeader>
<ModalCloseButton />
<ModalBody px="4" pt="0" pb="4">
<Flex gap="2" direction="column">
{events.map((event) => (
<RelayReviewNote key={event.id} event={event} />
))}
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
}
const B = styled.span`
font-weight: bold;
`;
export const Metadata = ({ name, children }: { name: string } & PropsWithChildren) =>
children ? (
<div>
<B>{name}: </B>
<span>{children}</span>
</div>
) : null;
export function RelayMetadata({ url }: { url: string }) {
const { info } = useRelayInfo(url);
return (
<Box>
<Metadata name="Name">{info?.name}</Metadata>
{info?.pubkey && (
<Flex gap="2" alignItems="center">
<B>Owner:</B>
<UserAvatar pubkey={info.pubkey} size="xs" />
<UserLink pubkey={info.pubkey} />
<UserDnsIdentityIcon pubkey={info.pubkey} onlyIcon />
</Flex>
)}
</Box>
);
}
export default function RelayCard({ url }: { url: string }) {
const account = useCurrentAccount();
const { info } = useRelayInfo(url);
const clientRelays = useClientRelays();
const reviewsModal = useDisclosure();
const debugModal = useDisclosure();
const joined = clientRelays.some((r) => r.url === url);
return (
<>
<Card>
<CardHeader display="flex" gap="2" alignItems="center" p="2">
<RelayFavicon relay={url} size="xs" />
<Heading size="md" isTruncated>
{url}
</Heading>
</CardHeader>
<CardBody px="2" py="0" display="flex" flexDirection="column" gap="2">
<RelayMetadata url={url} />
</CardBody>
<CardFooter p="2" as={Flex} gap="2">
{joined ? (
<Button
colorScheme="red"
variant="outline"
onClick={() => clientRelaysService.removeRelay(url)}
isDisabled={!account}
size="sm"
>
Leave
</Button>
) : (
<Button
colorScheme="green"
onClick={() => clientRelaysService.addRelay(url, RelayMode.ALL)}
isDisabled={!account}
size="sm"
>
Join
</Button>
)}
<Button onClick={reviewsModal.onOpen} size="sm">
Reviews
</Button>
<Button as={RouterLink} to={`/global?relay=${url}`} size="sm">
Notes
</Button>
<IconButton
icon={<CodeIcon />}
aria-label="Show JSON"
onClick={debugModal.onToggle}
variant="ghost"
size="sm"
ml="auto"
/>
<Button
as="a"
href={`https://nostr.watch/relay/${new URL(url).host}`}
target="_blank"
rightIcon={<ExternalLinkIcon />}
size="sm"
>
More
</Button>
</CardFooter>
</Card>
{reviewsModal.isOpen && <RelayReviewsModal isOpen onClose={reviewsModal.onClose} relay={url} size="2xl" />}
{debugModal.isOpen && (
<Modal isOpen onClose={debugModal.onClose} size="4xl">
<ModalOverlay />
<ModalContent>
<ModalHeader p="4">Relay Info</ModalHeader>
<ModalCloseButton />
<ModalBody px="4" pt="0" pb="4">
<RawJson heading="Info" json={info} />
</ModalBody>
</ModalContent>
</Modal>
)}
</>
);
}

View File

@ -0,0 +1,30 @@
import dayjs from "dayjs";
import { Box, Card, CardBody, CardHeader, Spacer, Text } from "@chakra-ui/react";
import { UserAvatarLink } from "../../components/user-avatar-link";
import { UserLink } from "../../components/user-link";
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
import StarRating from "../../components/star-rating";
import { safeJson } from "../../helpers/parse";
import { NostrEvent } from "../../types/nostr-event";
export default function RelayReviewNote({ event }: { event: NostrEvent }) {
const ratingJson = event.tags.find((t) => t[0] === "l" && t[3])?.[3];
const rating = ratingJson ? (safeJson(ratingJson, undefined) as { quality: number } | undefined) : undefined;
return (
<Card variant="outline">
<CardHeader display="flex" gap="2" px="2" pt="2" pb="0">
<UserAvatarLink pubkey={event.pubkey} size="xs" />
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
<Spacer />
<Text>{dayjs.unix(event.created_at).fromNow()}</Text>
</CardHeader>
<CardBody p="2" gap="2" display="flex" flexDirection="column">
{rating && <StarRating quality={rating.quality} color="yellow.400" />}
<Box whiteSpace="pre-wrap">{event.content}</Box>
</CardBody>
</Card>
);
}