mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-01 00:19:45 +02:00
add relay view
This commit is contained in:
parent
e24e55c8d9
commit
fa30250a20
5
.changeset/good-dryers-thank.md
Normal file
5
.changeset/good-dryers-thank.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add relay view
|
@ -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 /> },
|
||||
|
@ -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>
|
||||
|
@ -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" />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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
139
src/views/relays/relay.tsx
Normal 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} />;
|
||||
}
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user