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) => (