diff --git a/.changeset/friendly-needles-flash.md b/.changeset/friendly-needles-flash.md
new file mode 100644
index 000000000..9d571f3f5
--- /dev/null
+++ b/.changeset/friendly-needles-flash.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Add popular relays view
diff --git a/src/app.tsx b/src/app.tsx
index 79f4c2b8d..9f99cf58a 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -59,6 +59,7 @@ import CommunitiesHomeView from "./views/communities";
import CommunityFindByNameView from "./views/community/find-by-name";
import CommunityView from "./views/community/index";
import StreamModerationView from "./views/tools/stream-moderation";
+import PopularRelaysView from "./views/relays/popular";
const NetworkView = React.lazy(() => import("./views/tools/network"));
const NetworkGraphView = React.lazy(() => import("./views/tools/network-mute-graph"));
@@ -160,8 +161,14 @@ const router = createHashRouter([
element: ,
},
{ path: "settings", element: },
- { path: "relays/reviews", element: },
- { path: "relays", element: },
+ {
+ path: "relays",
+ children: [
+ { path: "", element: },
+ { path: "popular", element: },
+ { path: "reviews", element: },
+ ],
+ },
{ path: "r/:relay", element: },
{ path: "notifications", element: },
{ path: "search", element: },
diff --git a/src/views/relays/components/relay-card.tsx b/src/views/relays/components/relay-card.tsx
index 9f2f85462..5bde38e17 100644
--- a/src/views/relays/components/relay-card.tsx
+++ b/src/views/relays/components/relay-card.tsx
@@ -28,7 +28,7 @@ import { Link as RouterLink } from "react-router-dom";
import { useRelayInfo } from "../../../hooks/use-relay-info";
import { RelayFavicon } from "../../../components/relay-favicon";
-import { CodeIcon, RepostIcon } from "../../../components/icons";
+import { CodeIcon } from "../../../components/icons";
import { UserLink } from "../../../components/user-link";
import { UserAvatar } from "../../../components/user-avatar";
import { useClientRelays } from "../../../hooks/use-client-relays";
@@ -160,7 +160,7 @@ export default function RelayCard({ url, ...props }: { url: string } & Omit
-
+
{url}
diff --git a/src/views/relays/index.tsx b/src/views/relays/index.tsx
index d3af60442..fe7e1a52d 100644
--- a/src/views/relays/index.tsx
+++ b/src/views/relays/index.tsx
@@ -25,6 +25,7 @@ export default function RelaysView() {
.filter((r) => !clientRelays.includes(r.url))
.map((r) => r.url)
.filter(safeRelayUrl);
+
const { value: onlineRelays = [] } = useAsync(async () =>
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise),
);
@@ -42,6 +43,9 @@ export default function RelaysView() {
setSearch(e.target.value)} w="auto" />
+
@@ -57,7 +61,7 @@ export default function RelaysView() {
))}
- {discoveredRelays && !isSearching && (
+ {discoveredRelays.length > 0 && !isSearching && (
<>
Discovered Relays
diff --git a/src/views/relays/popular.tsx b/src/views/relays/popular.tsx
new file mode 100644
index 000000000..2d6e8cf9b
--- /dev/null
+++ b/src/views/relays/popular.tsx
@@ -0,0 +1,106 @@
+import {
+ AvatarGroup,
+ Button,
+ Card,
+ CardBody,
+ CardHeader,
+ Flex,
+ Heading,
+ LinkBox,
+ LinkOverlay,
+ SimpleGrid,
+ Text,
+} from "@chakra-ui/react";
+import { memo } from "react";
+import { Link as RouterLink, useNavigate } from "react-router-dom";
+
+import VerticalPageLayout from "../../components/vertical-page-layout";
+import { getPubkeysFromList } from "../../helpers/nostr/lists";
+import { useClientRelays, useReadRelayUrls } from "../../hooks/use-client-relays";
+import { useCurrentAccount } from "../../hooks/use-current-account";
+import useSubjects from "../../hooks/use-subjects";
+import useUserContactList from "../../hooks/use-user-contact-list";
+import RequireCurrentAccount from "../../providers/require-current-account";
+import userRelaysService from "../../services/user-relays";
+import { NostrEvent } from "../../types/nostr-event";
+import { RelayFavicon } from "../../components/relay-favicon";
+import { ArrowLeftSIcon } from "../../components/icons";
+import { UserAvatar } from "../../components/user-avatar";
+import { RelayMetadata } from "./components/relay-card";
+
+function usePopularContactsRelays(list?: NostrEvent) {
+ const readRelays = useReadRelayUrls();
+ const subs = list ? getPubkeysFromList(list).map((p) => userRelaysService.requestRelays(p.pubkey, readRelays)) : [];
+ const contactsRelays = useSubjects(subs);
+
+ const relayScore: Record = {};
+ for (const { relays, pubkey } of contactsRelays) {
+ for (const { url } of relays) {
+ relayScore[url] = relayScore[url] || [];
+ relayScore[url].push(pubkey);
+ }
+ }
+
+ const relayUrls = Array.from(Object.entries(relayScore)).map(([url, pubkeys]) => ({ url, pubkeys }));
+
+ return relayUrls.sort((a, b) => b.pubkeys.length - a.pubkeys.length);
+}
+
+const RelayCard = memo(({ url, pubkeys }: { url: string; pubkeys: string[] }) => {
+ return (
+
+
+
+
+
+ {url}
+
+
+
+
+
+ Used by {pubkeys.length} contacts:
+
+ {pubkeys.map((pubkey) => (
+
+ ))}
+
+
+
+ );
+});
+
+function PopularRelaysPage() {
+ const navigate = useNavigate();
+ const account = useCurrentAccount();
+ const contacts = useUserContactList(account?.pubkey);
+
+ const clientRelays = useClientRelays().map((r) => r.url);
+ const popularRelays = usePopularContactsRelays(contacts).filter(
+ (r) => !clientRelays.includes(r.url) && r.pubkeys.length > 1,
+ );
+
+ return (
+
+
+
+ Popular Relays
+
+
+ {popularRelays.map(({ url, pubkeys }) => (
+
+ ))}
+
+
+ );
+}
+
+export default function PopularRelaysView() {
+ return (
+
+
+
+ );
+}
diff --git a/src/views/relays/reviews.tsx b/src/views/relays/reviews.tsx
index 9136bd5bf..052a373ae 100644
--- a/src/views/relays/reviews.tsx
+++ b/src/views/relays/reviews.tsx
@@ -1,4 +1,4 @@
-import { Button, Flex } from "@chakra-ui/react";
+import { Button, Flex, Heading } from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
@@ -35,11 +35,12 @@ function RelayReviewsPage() {
return (
-
+
+ Relay Reviews
{reviews.map((event) => (