add relay view

This commit is contained in:
hzrd149 2023-08-05 12:32:01 -05:00
parent e24e55c8d9
commit fa30250a20
7 changed files with 188 additions and 74 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add relay view

View File

@ -36,6 +36,7 @@ import UserStreamsTab from "./views/user/streams";
import { PageProviders } from "./providers"; import { PageProviders } from "./providers";
import RelaysView from "./views/relays"; import RelaysView from "./views/relays";
import RelayReviewsView from "./views/relays/reviews"; import RelayReviewsView from "./views/relays/reviews";
import RelayView from "./views/relays/relay";
const StreamsView = React.lazy(() => import("./views/streams")); const StreamsView = React.lazy(() => import("./views/streams"));
const StreamView = React.lazy(() => import("./views/streams/stream")); const StreamView = React.lazy(() => import("./views/streams/stream"));
@ -107,6 +108,7 @@ const router = createHashRouter([
{ path: "settings", element: <SettingsView /> }, { path: "settings", element: <SettingsView /> },
{ path: "relays/reviews", element: <RelayReviewsView /> }, { path: "relays/reviews", element: <RelayReviewsView /> },
{ path: "relays", element: <RelaysView /> }, { path: "relays", element: <RelaysView /> },
{ path: "r/:relay", element: <RelayView /> },
{ path: "notifications", element: <NotificationsView /> }, { path: "notifications", element: <NotificationsView /> },
{ path: "search", element: <SearchView /> }, { path: "search", element: <SearchView /> },
{ path: "dm", element: <DirectMessagesView /> }, { path: "dm", element: <DirectMessagesView /> },

View File

@ -1,18 +1,19 @@
import { import {
Avatar,
Box,
Flex, Flex,
FlexProps, FlexProps,
IconButton, Link,
Modal, Modal,
ModalBody, ModalBody,
ModalCloseButton, ModalCloseButton,
ModalContent, ModalContent,
ModalHeader, ModalHeader,
ModalOverlay, ModalOverlay,
Tag,
Text, Text,
useDisclosure, useDisclosure,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { RelayFavicon } from "./relay-favicon"; import { RelayFavicon } from "./relay-favicon";
import relayScoreboardService from "../services/relay-scoreboard"; import relayScoreboardService from "../services/relay-scoreboard";
@ -34,18 +35,27 @@ export function RelayIconStack({ relays, maxRelays, ...props }: { relays: string
</Text> </Text>
)} )}
</Flex> </Flex>
<Modal isOpen={isOpen} onClose={onClose}> <Modal isOpen={isOpen} onClose={onClose} size="lg">
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
<ModalHeader>Relays</ModalHeader> <ModalHeader px="4" pt="4" pb="2">
Seen on relays:
</ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody px="4" pb="4" pt="0">
<Flex direction="column" gap="1"> <Flex gap="2" wrap="wrap">
{topRelays.map((url) => ( {topRelays.map((url) => (
<Flex key={url}> <Tag
<RelayFavicon relay={url} size="2xs" mr="2" /> key={url}
<Text>{url}</Text> as={RouterLink}
</Flex> p="2"
fontWeight="bold"
fontSize="md"
to={`/r/${encodeURIComponent(url)}`}
>
<RelayFavicon relay={url} size="xs" mr="2" />
{url}
</Tag>
))} ))}
</Flex> </Flex>
</ModalBody> </ModalBody>

View File

@ -1,13 +1,12 @@
import { import {
Box, Box,
Button, Button,
ButtonGroup, ButtonProps,
Card, Card,
CardBody, CardBody,
CardFooter, CardFooter,
CardHeader, CardHeader,
CardProps, CardProps,
Code,
Flex, Flex,
Heading, Heading,
IconButton, IconButton,
@ -19,8 +18,6 @@ import {
ModalHeader, ModalHeader,
ModalOverlay, ModalOverlay,
ModalProps, ModalProps,
Spacer,
Text,
useDisclosure, useDisclosure,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom";
@ -41,36 +38,6 @@ import styled from "@emotion/styled";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
import RawJson from "../../../components/debug-modals/raw-json"; 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} hideUrl />
))}
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
}
const B = styled.span` const B = styled.span`
font-weight: bold; font-weight: bold;
`; `;
@ -100,7 +67,7 @@ export function RelayMetadata({ url }: { url: string }) {
); );
} }
export function RelayJoinAction({ url }: { url: string }) { export function RelayJoinAction({ url, ...props }: { url: string } & Omit<ButtonProps, "children" | "onClick">) {
const account = useCurrentAccount(); const account = useCurrentAccount();
const clientRelays = useClientRelays(); const clientRelays = useClientRelays();
const joined = clientRelays.some((r) => r.url === url); const joined = clientRelays.some((r) => r.url === url);
@ -111,7 +78,7 @@ export function RelayJoinAction({ url }: { url: string }) {
variant="outline" variant="outline"
onClick={() => clientRelaysService.removeRelay(url)} onClick={() => clientRelaysService.removeRelay(url)}
isDisabled={!account} isDisabled={!account}
size="sm" {...props}
> >
Leave Leave
</Button> </Button>
@ -120,26 +87,19 @@ export function RelayJoinAction({ url }: { url: string }) {
colorScheme="green" colorScheme="green"
onClick={() => clientRelaysService.addRelay(url, RelayMode.ALL)} onClick={() => clientRelaysService.addRelay(url, RelayMode.ALL)}
isDisabled={!account} isDisabled={!account}
size="sm" {...props}
> >
Join Join
</Button> </Button>
); );
} }
export function DebugButton({ url, ...props }: { url: string } & Omit<IconButtonProps, "icon" | "aria-label">) { export function RelayDebugButton({ url, ...props }: { url: string } & Omit<IconButtonProps, "icon" | "aria-label">) {
const { info } = useRelayInfo(url); const { info } = useRelayInfo(url);
const debugModal = useDisclosure(); const debugModal = useDisclosure();
return ( return (
<> <>
<IconButton <IconButton icon={<CodeIcon />} aria-label="Show JSON" onClick={debugModal.onToggle} variant="ghost" {...props} />
icon={<CodeIcon />}
aria-label="Show JSON"
onClick={debugModal.onToggle}
variant="ghost"
size="sm"
{...props}
/>
{debugModal.isOpen && ( {debugModal.isOpen && (
<Modal isOpen onClose={debugModal.onClose} size="4xl"> <Modal isOpen onClose={debugModal.onClose} size="4xl">
<ModalOverlay /> <ModalOverlay />
@ -157,30 +117,22 @@ export function DebugButton({ url, ...props }: { url: string } & Omit<IconButton
} }
export default function RelayCard({ url, ...props }: { url: string } & Omit<CardProps, "children">) { export default function RelayCard({ url, ...props }: { url: string } & Omit<CardProps, "children">) {
const reviewsModal = useDisclosure();
return ( return (
<> <>
<Card {...props}> <Card {...props}>
<CardHeader display="flex" gap="2" alignItems="center" p="2"> <CardHeader display="flex" gap="2" alignItems="center" p="2">
<RelayFavicon relay={url} size="xs" /> <RelayFavicon relay={url} size="xs" />
<Heading size="md" isTruncated> <Heading size="md" isTruncated>
{url} <RouterLink to={`/r/${encodeURIComponent(url)}`}>{url}</RouterLink>
</Heading> </Heading>
</CardHeader> </CardHeader>
<CardBody px="2" py="0" display="flex" flexDirection="column" gap="2"> <CardBody px="2" py="0" display="flex" flexDirection="column" gap="2">
<RelayMetadata url={url} /> <RelayMetadata url={url} />
</CardBody> </CardBody>
<CardFooter p="2" as={Flex} gap="2"> <CardFooter p="2" as={Flex} gap="2">
<RelayJoinAction url={url} /> <RelayJoinAction url={url} size="sm" />
<Button onClick={reviewsModal.onOpen} size="sm">
Reviews
</Button>
<Button as={RouterLink} to={`/global?relay=${url}`} size="sm">
Notes
</Button>
<DebugButton url={url} ml="auto" /> <RelayDebugButton url={url} ml="auto" size="sm" />
<Button <Button
as="a" as="a"
href={`https://nostr.watch/relay/${new URL(url).host}`} href={`https://nostr.watch/relay/${new URL(url).host}`}
@ -192,7 +144,6 @@ export default function RelayCard({ url, ...props }: { url: string } & Omit<Card
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
{reviewsModal.isOpen && <RelayReviewsModal isOpen onClose={reviewsModal.onClose} relay={url} size="2xl" />}
</> </>
); );
} }

View File

@ -1,6 +1,7 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useRef } from "react"; import { useRef } from "react";
import { Card, CardBody, CardHeader, Text } from "@chakra-ui/react"; import { Card, CardBody, CardHeader, Link, Text } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { UserAvatarLink } from "../../../components/user-avatar-link"; import { UserAvatarLink } from "../../../components/user-avatar-link";
import { UserLink } from "../../../components/user-link"; import { UserLink } from "../../../components/user-link";
@ -30,7 +31,13 @@ export default function RelayReviewNote({ event, hideUrl }: { event: NostrEvent;
<Text ml="auto">{dayjs.unix(event.created_at).fromNow()}</Text> <Text ml="auto">{dayjs.unix(event.created_at).fromNow()}</Text>
</CardHeader> </CardHeader>
<CardBody p="2"> <CardBody p="2">
{!hideUrl && <Metadata name="URL">{url}</Metadata>} {!hideUrl && url && (
<Metadata name="URL">
<Link as={RouterLink} to={`/r/${encodeURIComponent(url)}`}>
{url}
</Link>
</Metadata>
)}
{rating && <StarRating quality={rating.quality} color="yellow.400" mb="1" />} {rating && <StarRating quality={rating.quality} color="yellow.400" mb="1" />}
<NoteContents event={event} /> <NoteContents event={event} />
</CardBody> </CardBody>

139
src/views/relays/relay.tsx Normal file
View File

@ -0,0 +1,139 @@
import { useParams } from "react-router-dom";
import { Box, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs, Tag, Tooltip } from "@chakra-ui/react";
import { safeRelayUrl } from "../../helpers/url";
import { useRelayInfo } from "../../hooks/use-relay-info";
import { RelayDebugButton, RelayJoinAction, RelayMetadata } from "./components/relay-card";
import useSubject from "../../hooks/use-subject";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import RelayReviewNote from "./components/relay-review-note";
// copied from github
const NIP_NAMES: Record<string, string> = {
"01": "Basic protocol",
"02": "Contact List and Petnames",
"03": "OpenTimestamps Attestations for Events",
"04": "Encrypted Direct Message",
"05": "Mapping Nostr keys to DNS-based internet identifiers",
"06": "Basic key derivation from mnemonic seed phrase",
"07": "window.nostr capability for web browsers",
"08": "Handling Mentions",
"09": "Event Deletion",
"10": "Conventions for clients' use of e and p tags in text events",
"11": "Relay Information Document",
"12": "Generic Tag Queries",
"13": "Proof of Work",
"14": "Subject tag in text events",
"15": "Nostr Marketplace",
"16": "Event Treatment",
"18": "Reposts",
"19": "bech32-encoded entities",
"20": "Command Results",
"21": "nostr: URI scheme",
"22": "Event created_at Limits",
"23": "Long-form Content",
"25": "Reactions",
"26": "Delegated Event Signing",
"27": "Text Note References",
"28": "Public Chat",
"30": "Custom Emoji",
"31": "Dealing with Unknown Events",
"32": "Labeling",
"33": "Parameterized Replaceable Events",
"36": "Sensitive Content",
"39": "External Identities in Profiles",
"40": "Expiration Timestamp",
"42": "Authentication of clients to relays",
"45": "Counting results",
"46": "Nostr Connect",
"47": "Wallet Connect",
"50": "Keywords filter",
"51": "Lists",
"52": "Calendar Events",
"53": "Live Activities",
"56": "Reporting",
"57": "Lightning Zaps",
"58": "Badges",
"65": "Relay List Metadata",
"78": "Application-specific data",
"89": "Recommended Application Handlers",
"94": "File Metadata",
"98": "HTTP Auth",
"99": "Classified Listings",
};
function NipTag({ nip }: { nip: number }) {
const nipStr = String(nip).padStart(2, "0");
return (
<Tooltip label={NIP_NAMES[nipStr]}>
<Tag as="a" target="_blank" href={`https://github.com/nostr-protocol/nips/blob/master/${nipStr}.md`}>
NIP-{nip}
</Tag>
</Tooltip>
);
}
function RelayReviews({ relay }: { relay: string }) {
const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader(`${relay}-reviews`, readRelays, {
kinds: [1985],
"#r": [relay],
"#l": ["review/relay"],
});
const events = useSubject(timeline.timeline);
return (
<Flex direction="column" gap="2">
{events.map((event) => (
<RelayReviewNote key={event.id} event={event} hideUrl />
))}
</Flex>
);
}
function RelayPage({ relay }: { relay: string }) {
const { info } = useRelayInfo(relay);
return (
<Flex direction="column" alignItems="stretch" gap="2" py="2">
<Flex gap="2" alignItems="center">
<Heading>{relay}</Heading>
<RelayDebugButton url={relay} ml="auto" />
<RelayJoinAction url={relay} />
</Flex>
<RelayMetadata url={relay} />
<Flex gap="2">
{info?.supported_nips?.map((nip) => (
<NipTag key={nip} nip={nip} />
))}
</Flex>
<Tabs display="flex" flexDirection="column" flexGrow="1" isLazy colorScheme="brand">
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
<Tab>Reviews</Tab>
<Tab isDisabled>Notes</Tab>
</TabList>
<TabPanels>
<TabPanel py="2" px="0">
<RelayReviews relay={relay} />
</TabPanel>
<TabPanel py="2" px="0"></TabPanel>
</TabPanels>
</Tabs>
</Flex>
);
}
export default function RelayView() {
const { relay } = useParams<string>();
if (!relay) return <>No relay url</>;
const safeUrl = safeRelayUrl(relay);
if (!safeUrl) return <>Bad relay url</>;
return <RelayPage relay={safeUrl} />;
}

View File

@ -8,7 +8,7 @@ import useSubject from "../../hooks/use-subject";
import { NostrEvent } from "../../types/nostr-event"; import { NostrEvent } from "../../types/nostr-event";
import RelayReviewNote from "../relays/components/relay-review-note"; import RelayReviewNote from "../relays/components/relay-review-note";
import { RelayFavicon } from "../../components/relay-favicon"; import { RelayFavicon } from "../../components/relay-favicon";
import { DebugButton, RelayJoinAction, RelayMetadata } from "../relays/components/relay-card"; import { RelayDebugButton, RelayJoinAction, RelayMetadata } from "../relays/components/relay-card";
import IntersectionObserverProvider from "../../providers/intersection-observer"; import IntersectionObserverProvider from "../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
@ -21,12 +21,12 @@ function Relay({ url, reviews }: { url: string; reviews: NostrEvent[] }) {
{url} {url}
</Heading> </Heading>
<Spacer /> <Spacer />
<RelayJoinAction url={url} /> <RelayJoinAction url={url} size="sm" />
<Button as={RouterLink} to={`/global?relay=${url}`} size="sm"> <Button as={RouterLink} to={`/global?relay=${url}`} size="sm">
Notes Notes
</Button> </Button>
<DebugButton url={url} /> <RelayDebugButton url={url} size="sm" />
</Flex> </Flex>
<RelayMetadata url={url} /> <RelayMetadata url={url} />
<Flex py="0" direction="column" gap="2"> <Flex py="0" direction="column" gap="2">