diff --git a/.changeset/good-dryers-thank.md b/.changeset/good-dryers-thank.md
new file mode 100644
index 000000000..ada58adf5
--- /dev/null
+++ b/.changeset/good-dryers-thank.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Add relay view
diff --git a/src/app.tsx b/src/app.tsx
index e21cde823..ae21fd0b1 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -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: },
{ path: "relays/reviews", element: },
{ path: "relays", element: },
+ { path: "r/:relay", element: },
{ path: "notifications", element: },
{ path: "search", element: },
{ path: "dm", element: },
diff --git a/src/components/relay-icon-stack.tsx b/src/components/relay-icon-stack.tsx
index a0633df1d..4e31ecac9 100644
--- a/src/components/relay-icon-stack.tsx
+++ b/src/components/relay-icon-stack.tsx
@@ -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
)}
-
+
- Relays
+
+ Seen on relays:
+
-
-
+
+
{topRelays.map((url) => (
-
-
- {url}
-
+
+
+ {url}
+
))}
diff --git a/src/views/relays/components/relay-card.tsx b/src/views/relays/components/relay-card.tsx
index 71b7ecc95..9cacb30ef 100644
--- a/src/views/relays/components/relay-card.tsx
+++ b/src/views/relays/components/relay-card.tsx
@@ -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) {
- const readRelays = useReadRelayUrls();
- const timeline = useTimelineLoader(`${relay}-reviews`, readRelays, {
- kinds: [1985],
- "#r": [relay],
- "#l": ["review/relay"],
- });
-
- const events = useSubject(timeline.timeline);
-
- return (
-
-
-
-
- {relay} reviews
-
-
-
-
- {events.map((event) => (
-
- ))}
-
-
-
-
- );
-}
-
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) {
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
@@ -120,26 +87,19 @@ export function RelayJoinAction({ url }: { url: string }) {
colorScheme="green"
onClick={() => clientRelaysService.addRelay(url, RelayMode.ALL)}
isDisabled={!account}
- size="sm"
+ {...props}
>
Join
);
}
-export function DebugButton({ url, ...props }: { url: string } & Omit) {
+export function RelayDebugButton({ url, ...props }: { url: string } & Omit) {
const { info } = useRelayInfo(url);
const debugModal = useDisclosure();
return (
<>
- }
- aria-label="Show JSON"
- onClick={debugModal.onToggle}
- variant="ghost"
- size="sm"
- {...props}
- />
+ } aria-label="Show JSON" onClick={debugModal.onToggle} variant="ghost" {...props} />
{debugModal.isOpen && (
@@ -157,30 +117,22 @@ export function DebugButton({ url, ...props }: { url: string } & Omit) {
- const reviewsModal = useDisclosure();
-
return (
<>
- {url}
+ {url}
-
-
-
+
-
+
- {reviewsModal.isOpen && }
>
);
}
diff --git a/src/views/relays/components/relay-review-note.tsx b/src/views/relays/components/relay-review-note.tsx
index 7cd5cd847..1495c25be 100644
--- a/src/views/relays/components/relay-review-note.tsx
+++ b/src/views/relays/components/relay-review-note.tsx
@@ -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;
{dayjs.unix(event.created_at).fromNow()}
- {!hideUrl && {url}}
+ {!hideUrl && url && (
+
+
+ {url}
+
+
+ )}
{rating && }
diff --git a/src/views/relays/relay.tsx b/src/views/relays/relay.tsx
new file mode 100644
index 000000000..739ce6786
--- /dev/null
+++ b/src/views/relays/relay.tsx
@@ -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 = {
+ "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 (
+
+
+ NIP-{nip}
+
+
+ );
+}
+
+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 (
+
+ {events.map((event) => (
+
+ ))}
+
+ );
+}
+
+function RelayPage({ relay }: { relay: string }) {
+ const { info } = useRelayInfo(relay);
+
+ return (
+
+
+ {relay}
+
+
+
+
+
+ {info?.supported_nips?.map((nip) => (
+
+ ))}
+
+
+
+ Reviews
+ Notes
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default function RelayView() {
+ const { relay } = useParams();
+ if (!relay) return <>No relay url>;
+
+ const safeUrl = safeRelayUrl(relay);
+
+ if (!safeUrl) return <>Bad relay url>;
+
+ return ;
+}
diff --git a/src/views/user/relays.tsx b/src/views/user/relays.tsx
index 8afb7bfb1..86c3d7a1d 100644
--- a/src/views/user/relays.tsx
+++ b/src/views/user/relays.tsx
@@ -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}
-
+
-
+