mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-13 06:09:42 +02:00
show more relay info
This commit is contained in:
parent
33da3e2fa9
commit
a60408220e
@ -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);
|
||||
|
105
src/views/relays/add-custom-modal.tsx
Normal file
105
src/views/relays/add-custom-modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
184
src/views/relays/relay-card.tsx
Normal file
184
src/views/relays/relay-card.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
30
src/views/relays/relay-review-note.tsx
Normal file
30
src/views/relays/relay-review-note.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user