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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import dayjs from "dayjs";
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 { 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>
</CardHeader>
<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" />}
<NoteContents event={event} />
</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 RelayReviewNote from "../relays/components/relay-review-note";
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 { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
@ -21,12 +21,12 @@ function Relay({ url, reviews }: { url: string; reviews: NostrEvent[] }) {
{url}
</Heading>
<Spacer />
<RelayJoinAction url={url} />
<RelayJoinAction url={url} size="sm" />
<Button as={RouterLink} to={`/global?relay=${url}`} size="sm">
Notes
</Button>
<DebugButton url={url} />
<RelayDebugButton url={url} size="sm" />
</Flex>
<RelayMetadata url={url} />
<Flex py="0" direction="column" gap="2">