mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-09 20:33:03 +02:00
add popular relays view
This commit is contained in:
5
.changeset/friendly-needles-flash.md
Normal file
5
.changeset/friendly-needles-flash.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add popular relays view
|
11
src/app.tsx
11
src/app.tsx
@@ -59,6 +59,7 @@ import CommunitiesHomeView from "./views/communities";
|
|||||||
import CommunityFindByNameView from "./views/community/find-by-name";
|
import CommunityFindByNameView from "./views/community/find-by-name";
|
||||||
import CommunityView from "./views/community/index";
|
import CommunityView from "./views/community/index";
|
||||||
import StreamModerationView from "./views/tools/stream-moderation";
|
import StreamModerationView from "./views/tools/stream-moderation";
|
||||||
|
import PopularRelaysView from "./views/relays/popular";
|
||||||
|
|
||||||
const NetworkView = React.lazy(() => import("./views/tools/network"));
|
const NetworkView = React.lazy(() => import("./views/tools/network"));
|
||||||
const NetworkGraphView = React.lazy(() => import("./views/tools/network-mute-graph"));
|
const NetworkGraphView = React.lazy(() => import("./views/tools/network-mute-graph"));
|
||||||
@@ -160,8 +161,14 @@ const router = createHashRouter([
|
|||||||
element: <NoteView />,
|
element: <NoteView />,
|
||||||
},
|
},
|
||||||
{ path: "settings", element: <SettingsView /> },
|
{ path: "settings", element: <SettingsView /> },
|
||||||
{ path: "relays/reviews", element: <RelayReviewsView /> },
|
{
|
||||||
{ path: "relays", element: <RelaysView /> },
|
path: "relays",
|
||||||
|
children: [
|
||||||
|
{ path: "", element: <RelaysView /> },
|
||||||
|
{ path: "popular", element: <PopularRelaysView /> },
|
||||||
|
{ path: "reviews", element: <RelayReviewsView /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
{ path: "r/:relay", element: <RelayView /> },
|
{ path: "r/:relay", element: <RelayView /> },
|
||||||
{ path: "notifications", element: <NotificationsView /> },
|
{ path: "notifications", element: <NotificationsView /> },
|
||||||
{ path: "search", element: <SearchView /> },
|
{ path: "search", element: <SearchView /> },
|
||||||
|
@@ -28,7 +28,7 @@ import { Link as RouterLink } from "react-router-dom";
|
|||||||
|
|
||||||
import { useRelayInfo } from "../../../hooks/use-relay-info";
|
import { useRelayInfo } from "../../../hooks/use-relay-info";
|
||||||
import { RelayFavicon } from "../../../components/relay-favicon";
|
import { RelayFavicon } from "../../../components/relay-favicon";
|
||||||
import { CodeIcon, RepostIcon } from "../../../components/icons";
|
import { CodeIcon } from "../../../components/icons";
|
||||||
import { UserLink } from "../../../components/user-link";
|
import { UserLink } from "../../../components/user-link";
|
||||||
import { UserAvatar } from "../../../components/user-avatar";
|
import { UserAvatar } from "../../../components/user-avatar";
|
||||||
import { useClientRelays } from "../../../hooks/use-client-relays";
|
import { useClientRelays } from "../../../hooks/use-client-relays";
|
||||||
@@ -160,7 +160,7 @@ export default function RelayCard({ url, ...props }: { url: string } & Omit<Card
|
|||||||
<>
|
<>
|
||||||
<Card variant="outline" {...props}>
|
<Card variant="outline" {...props}>
|
||||||
<CardHeader display="flex" gap="2" alignItems="center" p="2">
|
<CardHeader display="flex" gap="2" alignItems="center" p="2">
|
||||||
<RelayFavicon relay={url} size="xs" />
|
<RelayFavicon relay={url} size="sm" />
|
||||||
<Heading size="md" isTruncated>
|
<Heading size="md" isTruncated>
|
||||||
<RouterLink to={`/r/${encodeURIComponent(url)}`}>{url}</RouterLink>
|
<RouterLink to={`/r/${encodeURIComponent(url)}`}>{url}</RouterLink>
|
||||||
<RelayPaidTag url={url} />
|
<RelayPaidTag url={url} />
|
||||||
|
@@ -25,6 +25,7 @@ export default function RelaysView() {
|
|||||||
.filter((r) => !clientRelays.includes(r.url))
|
.filter((r) => !clientRelays.includes(r.url))
|
||||||
.map((r) => r.url)
|
.map((r) => r.url)
|
||||||
.filter(safeRelayUrl);
|
.filter(safeRelayUrl);
|
||||||
|
|
||||||
const { value: onlineRelays = [] } = useAsync(async () =>
|
const { value: onlineRelays = [] } = useAsync(async () =>
|
||||||
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise<string[]>),
|
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise<string[]>),
|
||||||
);
|
);
|
||||||
@@ -42,6 +43,9 @@ export default function RelaysView() {
|
|||||||
<Flex alignItems="center" gap="2" wrap="wrap">
|
<Flex alignItems="center" gap="2" wrap="wrap">
|
||||||
<Input type="search" placeholder="search" value={search} onChange={(e) => setSearch(e.target.value)} w="auto" />
|
<Input type="search" placeholder="search" value={search} onChange={(e) => setSearch(e.target.value)} w="auto" />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
<Button as={RouterLink} to="/relays/popular">
|
||||||
|
Popular Relays
|
||||||
|
</Button>
|
||||||
<Button as={RouterLink} to="/relays/reviews">
|
<Button as={RouterLink} to="/relays/reviews">
|
||||||
Browse Reviews
|
Browse Reviews
|
||||||
</Button>
|
</Button>
|
||||||
@@ -57,7 +61,7 @@ export default function RelaysView() {
|
|||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
{discoveredRelays && !isSearching && (
|
{discoveredRelays.length > 0 && !isSearching && (
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Heading size="lg">Discovered Relays</Heading>
|
<Heading size="lg">Discovered Relays</Heading>
|
||||||
|
106
src/views/relays/popular.tsx
Normal file
106
src/views/relays/popular.tsx
Normal file
@@ -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<string, string[]> = {};
|
||||||
|
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 (
|
||||||
|
<Card variant="outline" as={LinkBox}>
|
||||||
|
<CardHeader px="2" pt="2" pb="0" display="flex" gap="2" alignItems="center">
|
||||||
|
<RelayFavicon relay={url} size="sm" />
|
||||||
|
<Heading size="md" isTruncated>
|
||||||
|
<LinkOverlay as={RouterLink} to={`/r/${encodeURIComponent(url)}`}>
|
||||||
|
{url}
|
||||||
|
</LinkOverlay>
|
||||||
|
</Heading>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody p="2">
|
||||||
|
<RelayMetadata url={url} />
|
||||||
|
<Text>Used by {pubkeys.length} contacts:</Text>
|
||||||
|
<AvatarGroup size="sm" max={10}>
|
||||||
|
{pubkeys.map((pubkey) => (
|
||||||
|
<UserAvatar key={pubkey} pubkey={pubkey} />
|
||||||
|
))}
|
||||||
|
</AvatarGroup>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<VerticalPageLayout>
|
||||||
|
<Flex gap="2" alignItems="center">
|
||||||
|
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Heading size="md">Popular Relays</Heading>
|
||||||
|
</Flex>
|
||||||
|
<SimpleGrid columns={[1, 1, 1, 2, 3]} spacing="2">
|
||||||
|
{popularRelays.map(({ url, pubkeys }) => (
|
||||||
|
<RelayCard url={url} pubkeys={pubkeys} key={url} />
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</VerticalPageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PopularRelaysView() {
|
||||||
|
return (
|
||||||
|
<RequireCurrentAccount>
|
||||||
|
<PopularRelaysPage />
|
||||||
|
</RequireCurrentAccount>
|
||||||
|
);
|
||||||
|
}
|
@@ -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 { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
@@ -35,11 +35,12 @@ function RelayReviewsPage() {
|
|||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider callback={callback}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<VerticalPageLayout>
|
<VerticalPageLayout>
|
||||||
<Flex gap="2">
|
<Flex gap="2" alignItems="center">
|
||||||
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />}>
|
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<PeopleListSelection />
|
<PeopleListSelection />
|
||||||
|
<Heading size="md">Relay Reviews</Heading>
|
||||||
</Flex>
|
</Flex>
|
||||||
{reviews.map((event) => (
|
{reviews.map((event) => (
|
||||||
<RelayReviewNote key={event.id} event={event} />
|
<RelayReviewNote key={event.id} event={event} />
|
||||||
|
Reference in New Issue
Block a user