mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-05 02:20:26 +02:00
add relay reviews page
This commit is contained in:
parent
a60408220e
commit
e24e55c8d9
5
.changeset/four-snails-shout.md
Normal file
5
.changeset/four-snails-shout.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Show relay reviews under user relays tab
|
5
.changeset/polite-rockets-refuse.md
Normal file
5
.changeset/polite-rockets-refuse.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add relay reviews page
|
@ -35,6 +35,7 @@ import useSetColorMode from "./hooks/use-set-color-mode";
|
||||
import UserStreamsTab from "./views/user/streams";
|
||||
import { PageProviders } from "./providers";
|
||||
import RelaysView from "./views/relays";
|
||||
import RelayReviewsView from "./views/relays/reviews";
|
||||
|
||||
const StreamsView = React.lazy(() => import("./views/streams"));
|
||||
const StreamView = React.lazy(() => import("./views/streams/stream"));
|
||||
@ -104,6 +105,7 @@ const router = createHashRouter([
|
||||
element: <NoteView />,
|
||||
},
|
||||
{ path: "settings", element: <SettingsView /> },
|
||||
{ path: "relays/reviews", element: <RelayReviewsView /> },
|
||||
{ path: "relays", element: <RelaysView /> },
|
||||
{ path: "notifications", element: <NotificationsView /> },
|
||||
{ path: "search", element: <SearchView /> },
|
||||
|
@ -33,7 +33,7 @@ export default function EmbeddedNote({ note }: { note: NostrEvent }) {
|
||||
{dayjs.unix(note.created_at).fromNow()}
|
||||
</NoteLink>
|
||||
</CardHeader>
|
||||
<CardBody p="0">{expand.isOpen && <NoteContents event={note} />}</CardBody>
|
||||
<CardBody p="0">{expand.isOpen && <NoteContents px="2" event={note} />}</CardBody>
|
||||
</Card>
|
||||
</TrustProvider>
|
||||
);
|
||||
|
@ -12,5 +12,9 @@ export default function NoteContentWithWarning({ event }: { event: NostrEvent })
|
||||
const contentWarning = event.tags.find((t) => t[0] === "content-warning")?.[1];
|
||||
const showContentWarning = settings.showContentWarning && contentWarning && !expand?.expanded;
|
||||
|
||||
return showContentWarning ? <SensitiveContentWarning description={contentWarning} /> : <NoteContents event={event} />;
|
||||
return showContentWarning ? (
|
||||
<SensitiveContentWarning description={contentWarning} />
|
||||
) : (
|
||||
<NoteContents px="2" event={event} />
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { Box, BoxProps } from "@chakra-ui/react";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
||||
import {
|
||||
@ -54,12 +54,12 @@ export type NoteContentsProps = {
|
||||
event: NostrEvent | DraftNostrEvent;
|
||||
};
|
||||
|
||||
export const NoteContents = React.memo(({ event }: NoteContentsProps) => {
|
||||
export const NoteContents = React.memo(({ event, ...props }: NoteContentsProps & Omit<BoxProps, "children">) => {
|
||||
const content = buildContents(event);
|
||||
|
||||
return (
|
||||
<ImageGalleryProvider>
|
||||
<Box whiteSpace="pre-wrap" px="2">
|
||||
<Box whiteSpace="pre-wrap" {...props}>
|
||||
{content}
|
||||
</Box>
|
||||
</ImageGalleryProvider>
|
||||
|
@ -28,8 +28,6 @@ export function useTimelinePageEventFilter() {
|
||||
export type TimelineViewType = "timeline" | "images";
|
||||
|
||||
export default function TimelinePage({ timeline, header }: { timeline: TimelineLoader; header?: React.ReactNode }) {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
const [params, setParams] = useSearchParams();
|
||||
|
@ -20,13 +20,13 @@ import {
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
import { useRelayInfo } from "../../hooks/use-relay-info";
|
||||
import { UserAvatar } from "../../components/user-avatar";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { safeRelayUrl } from "../../helpers/url";
|
||||
import { useRelayInfo } from "../../../hooks/use-relay-info";
|
||||
import { UserAvatar } from "../../../components/user-avatar";
|
||||
import { UserLink } from "../../../components/user-link";
|
||||
import { safeRelayUrl } from "../../../helpers/url";
|
||||
import { useDebounce } from "react-use";
|
||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
||||
import { CodeIcon } from "../../components/icons";
|
||||
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
|
||||
import { CodeIcon } from "../../../components/icons";
|
||||
import { Metadata } from "./relay-card";
|
||||
|
||||
function RelayDetails({ url, debug }: { url: string; debug?: boolean }) {
|
@ -6,10 +6,12 @@ import {
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardProps,
|
||||
Code,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
IconButtonProps,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
@ -22,22 +24,22 @@ import {
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { useRelayInfo } from "../../hooks/use-relay-info";
|
||||
import { RelayFavicon } from "../../components/relay-favicon";
|
||||
import { CodeIcon, ExternalLinkIcon } from "../../components/icons";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { UserAvatar } from "../../components/user-avatar";
|
||||
import { useClientRelays, useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import { RelayMode } from "../../classes/relay";
|
||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useRelayInfo } from "../../../hooks/use-relay-info";
|
||||
import { RelayFavicon } from "../../../components/relay-favicon";
|
||||
import { CodeIcon, ExternalLinkIcon } from "../../../components/icons";
|
||||
import { UserLink } from "../../../components/user-link";
|
||||
import { UserAvatar } from "../../../components/user-avatar";
|
||||
import { useClientRelays, useReadRelayUrls } from "../../../hooks/use-client-relays";
|
||||
import clientRelaysService from "../../../services/client-relays";
|
||||
import { RelayMode } from "../../../classes/relay";
|
||||
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
||||
import RelayReviewNote from "./relay-review-note";
|
||||
import styled from "@emotion/styled";
|
||||
import { PropsWithChildren } from "react";
|
||||
import RawJson from "../../components/debug-modals/raw-json";
|
||||
import RawJson from "../../../components/debug-modals/raw-json";
|
||||
|
||||
function RelayReviewsModal({ relay, ...props }: { relay: string } & Omit<ModalProps, "children">) {
|
||||
const readRelays = useReadRelayUrls();
|
||||
@ -60,7 +62,7 @@ function RelayReviewsModal({ relay, ...props }: { relay: string } & Omit<ModalPr
|
||||
<ModalBody px="4" pt="0" pb="4">
|
||||
<Flex gap="2" direction="column">
|
||||
{events.map((event) => (
|
||||
<RelayReviewNote key={event.id} event={event} />
|
||||
<RelayReviewNote key={event.id} event={event} hideUrl />
|
||||
))}
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
@ -98,75 +100,46 @@ export function RelayMetadata({ url }: { url: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function RelayCard({ url }: { url: string }) {
|
||||
export function RelayJoinAction({ url }: { url: string }) {
|
||||
const account = useCurrentAccount();
|
||||
const { info } = useRelayInfo(url);
|
||||
const clientRelays = useClientRelays();
|
||||
const reviewsModal = useDisclosure();
|
||||
const debugModal = useDisclosure();
|
||||
|
||||
const joined = clientRelays.some((r) => r.url === url);
|
||||
|
||||
return joined ? (
|
||||
<Button
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
onClick={() => clientRelaysService.removeRelay(url)}
|
||||
isDisabled={!account}
|
||||
size="sm"
|
||||
>
|
||||
Leave
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
colorScheme="green"
|
||||
onClick={() => clientRelaysService.addRelay(url, RelayMode.ALL)}
|
||||
isDisabled={!account}
|
||||
size="sm"
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function DebugButton({ url, ...props }: { url: string } & Omit<IconButtonProps, "icon" | "aria-label">) {
|
||||
const { info } = useRelayInfo(url);
|
||||
const debugModal = useDisclosure();
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader display="flex" gap="2" alignItems="center" p="2">
|
||||
<RelayFavicon relay={url} size="xs" />
|
||||
<Heading size="md" isTruncated>
|
||||
{url}
|
||||
</Heading>
|
||||
</CardHeader>
|
||||
<CardBody px="2" py="0" display="flex" flexDirection="column" gap="2">
|
||||
<RelayMetadata url={url} />
|
||||
</CardBody>
|
||||
<CardFooter p="2" as={Flex} gap="2">
|
||||
{joined ? (
|
||||
<Button
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
onClick={() => clientRelaysService.removeRelay(url)}
|
||||
isDisabled={!account}
|
||||
size="sm"
|
||||
>
|
||||
Leave
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
colorScheme="green"
|
||||
onClick={() => clientRelaysService.addRelay(url, RelayMode.ALL)}
|
||||
isDisabled={!account}
|
||||
size="sm"
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={reviewsModal.onOpen} size="sm">
|
||||
Reviews
|
||||
</Button>
|
||||
<Button as={RouterLink} to={`/global?relay=${url}`} size="sm">
|
||||
Notes
|
||||
</Button>
|
||||
|
||||
<IconButton
|
||||
icon={<CodeIcon />}
|
||||
aria-label="Show JSON"
|
||||
onClick={debugModal.onToggle}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
ml="auto"
|
||||
/>
|
||||
<Button
|
||||
as="a"
|
||||
href={`https://nostr.watch/relay/${new URL(url).host}`}
|
||||
target="_blank"
|
||||
rightIcon={<ExternalLinkIcon />}
|
||||
size="sm"
|
||||
>
|
||||
More
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
{reviewsModal.isOpen && <RelayReviewsModal isOpen onClose={reviewsModal.onClose} relay={url} size="2xl" />}
|
||||
<IconButton
|
||||
icon={<CodeIcon />}
|
||||
aria-label="Show JSON"
|
||||
onClick={debugModal.onToggle}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
{debugModal.isOpen && (
|
||||
<Modal isOpen onClose={debugModal.onClose} size="4xl">
|
||||
<ModalOverlay />
|
||||
@ -182,3 +155,44 @@ export default function RelayCard({ url }: { url: string }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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}
|
||||
</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>
|
||||
|
||||
<DebugButton url={url} ml="auto" />
|
||||
<Button
|
||||
as="a"
|
||||
href={`https://nostr.watch/relay/${new URL(url).host}`}
|
||||
target="_blank"
|
||||
rightIcon={<ExternalLinkIcon />}
|
||||
size="sm"
|
||||
>
|
||||
More
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
{reviewsModal.isOpen && <RelayReviewsModal isOpen onClose={reviewsModal.onClose} relay={url} size="2xl" />}
|
||||
</>
|
||||
);
|
||||
}
|
39
src/views/relays/components/relay-review-note.tsx
Normal file
39
src/views/relays/components/relay-review-note.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useRef } from "react";
|
||||
import { Card, CardBody, CardHeader, Text } from "@chakra-ui/react";
|
||||
|
||||
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||
import { UserLink } from "../../../components/user-link";
|
||||
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
|
||||
import StarRating from "../../../components/star-rating";
|
||||
import { safeJson } from "../../../helpers/parse";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||
import { NoteContents } from "../../../components/note/note-contents";
|
||||
import { Metadata } from "./relay-card";
|
||||
|
||||
export default function RelayReviewNote({ event, hideUrl }: { event: NostrEvent; hideUrl?: boolean }) {
|
||||
const ratingJson = event.tags.find((t) => t[0] === "l" && t[3])?.[3];
|
||||
const rating = ratingJson ? (safeJson(ratingJson, undefined) as { quality: number } | undefined) : undefined;
|
||||
|
||||
const url = event.tags.find((t) => t[0] === "r")?.[1];
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, event.id);
|
||||
|
||||
return (
|
||||
<Card variant="outline" ref={ref}>
|
||||
<CardHeader display="flex" gap="2" px="2" pt="2" pb="0">
|
||||
<UserAvatarLink pubkey={event.pubkey} size="xs" />
|
||||
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||
<Text ml="auto">{dayjs.unix(event.created_at).fromNow()}</Text>
|
||||
</CardHeader>
|
||||
<CardBody p="2">
|
||||
{!hideUrl && <Metadata name="URL">{url}</Metadata>}
|
||||
{rating && <StarRating quality={rating.quality} color="yellow.400" mb="1" />}
|
||||
<NoteContents event={event} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
import { useDeferredValue, useMemo, useState } from "react";
|
||||
import { Button, Divider, Flex, Heading, Input, SimpleGrid, Spacer, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
import { useAsync } from "react-use";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Button, Divider, Flex, Heading, Input, SimpleGrid, Spacer, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import { useClientRelays } from "../../hooks/use-client-relays";
|
||||
import relayPoolService from "../../services/relay-pool";
|
||||
import { safeRelayUrl } from "../../helpers/url";
|
||||
import AddCustomRelayModal from "./add-custom-modal";
|
||||
import RelayCard from "./relay-card";
|
||||
import AddCustomRelayModal from "./components/add-custom-modal";
|
||||
import RelayCard from "./components/relay-card";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import { RelayMode } from "../../classes/relay";
|
||||
|
||||
@ -43,13 +44,16 @@ export default function RelaysView() {
|
||||
Show All
|
||||
</Switch>
|
||||
<Spacer />
|
||||
<Button as={RouterLink} to="/relays/reviews">
|
||||
Browse Reviews
|
||||
</Button>
|
||||
<Button colorScheme="brand" onClick={addRelayModal.onOpen}>
|
||||
Add Custom
|
||||
</Button>
|
||||
</Flex>
|
||||
<SimpleGrid minChildWidth="25rem" spacing="2">
|
||||
{filteredRelays.map((url) => (
|
||||
<RelayCard key={url} url={url} />
|
||||
<RelayCard key={url} url={url} variant="outline" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
@ -59,7 +63,7 @@ export default function RelaysView() {
|
||||
<Heading size="lg">Discovered Relays</Heading>
|
||||
<SimpleGrid minChildWidth="25rem" spacing="2">
|
||||
{discoveredRelays.map((url) => (
|
||||
<RelayCard key={url} url={url} />
|
||||
<RelayCard key={url} url={url} variant="outline" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
|
@ -1,30 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import { Box, Card, CardBody, CardHeader, Spacer, Text } from "@chakra-ui/react";
|
||||
|
||||
import { UserAvatarLink } from "../../components/user-avatar-link";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
||||
import StarRating from "../../components/star-rating";
|
||||
import { safeJson } from "../../helpers/parse";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
export default function RelayReviewNote({ event }: { event: NostrEvent }) {
|
||||
const ratingJson = event.tags.find((t) => t[0] === "l" && t[3])?.[3];
|
||||
const rating = ratingJson ? (safeJson(ratingJson, undefined) as { quality: number } | undefined) : undefined;
|
||||
|
||||
return (
|
||||
<Card variant="outline">
|
||||
<CardHeader display="flex" gap="2" px="2" pt="2" pb="0">
|
||||
<UserAvatarLink pubkey={event.pubkey} size="xs" />
|
||||
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||
<Spacer />
|
||||
<Text>{dayjs.unix(event.created_at).fromNow()}</Text>
|
||||
</CardHeader>
|
||||
<CardBody p="2" gap="2" display="flex" flexDirection="column">
|
||||
{rating && <StarRating quality={rating.quality} color="yellow.400" />}
|
||||
<Box whiteSpace="pre-wrap">{event.content}</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
35
src/views/relays/reviews.tsx
Normal file
35
src/views/relays/reviews.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Button, Flex } from "@chakra-ui/react";
|
||||
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import RelayReviewNote from "./components/relay-review-note";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function RelayReviewsView() {
|
||||
const navigate = useNavigate();
|
||||
const readRelays = useReadRelayUrls();
|
||||
const timeline = useTimelineLoader("relay-reviews", readRelays, {
|
||||
kinds: [1985],
|
||||
"#l": ["review/relay"],
|
||||
});
|
||||
|
||||
const reviews = useSubject(timeline.timeline);
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider<string> callback={callback}>
|
||||
<Flex direction="column" gap="2" py="2">
|
||||
<Flex>
|
||||
<Button onClick={() => navigate(-1)}>Back</Button>
|
||||
</Flex>
|
||||
{reviews.map((event) => (
|
||||
<RelayReviewNote key={event.id} event={event} />
|
||||
))}
|
||||
</Flex>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
@ -1,38 +1,85 @@
|
||||
import { Text, Box, IconButton, Flex, Badge } from "@chakra-ui/react";
|
||||
import { useNavigate, useOutletContext } from "react-router-dom";
|
||||
import { GlobalIcon } from "../../components/icons";
|
||||
import { RelayMode } from "../../classes/relay";
|
||||
import { RelayScoreBreakdown } from "../../components/relay-score-breakdown";
|
||||
import useRankedRelayConfigs from "../../hooks/use-ranked-relay-configs";
|
||||
import { useOutletContext, Link as RouterLink } from "react-router-dom";
|
||||
import { Button, Flex, Heading, Spacer, StackDivider, VStack } from "@chakra-ui/react";
|
||||
import { useUserRelays } from "../../hooks/use-user-relays";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { truncatedId } from "../../helpers/nostr/event";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
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 IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
|
||||
function Relay({ url, reviews }: { url: string; reviews: NostrEvent[] }) {
|
||||
return (
|
||||
<Flex p="2" gap="2" direction="column">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<RelayFavicon relay={url} size="xs" />
|
||||
<Heading size="md" isTruncated>
|
||||
{url}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<RelayJoinAction url={url} />
|
||||
<Button as={RouterLink} to={`/global?relay=${url}`} size="sm">
|
||||
Notes
|
||||
</Button>
|
||||
|
||||
<DebugButton url={url} />
|
||||
</Flex>
|
||||
<RelayMetadata url={url} />
|
||||
<Flex py="0" direction="column" gap="2">
|
||||
{reviews.map((event) => (
|
||||
<RelayReviewNote key={event.id} event={event} />
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function getRelayReviews(url: string, events: NostrEvent[]) {
|
||||
return events.filter((e) => e.tags.some((t) => t[0] === "r" && t[1] === url));
|
||||
}
|
||||
|
||||
const UserRelaysTab = () => {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const userRelays = useUserRelays(pubkey);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const ranked = useRankedRelayConfigs(userRelays);
|
||||
const readRelays = useReadRelayUrls(userRelays.map((r) => r.url));
|
||||
const timeline = useTimelineLoader(`${truncatedId(pubkey)}-relay-reviews`, readRelays, {
|
||||
authors: [pubkey],
|
||||
kinds: [1985],
|
||||
"#l": ["review/relay"],
|
||||
});
|
||||
|
||||
const reviews = useSubject(timeline.timeline);
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
const otherReviews = reviews.filter((e) => {
|
||||
const url = e.tags.find((t) => t[0] === "r")?.[1];
|
||||
return !userRelays.some((r) => r.url === url);
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
{ranked.map((relayConfig) => (
|
||||
<Box key={relayConfig.url} display="flex" gap="2" alignItems="center" pr="2" pl="2">
|
||||
<Text flex={1} isTruncated>
|
||||
{relayConfig.url}
|
||||
</Text>
|
||||
<RelayScoreBreakdown relay={relayConfig.url} />
|
||||
{relayConfig.mode & RelayMode.WRITE ? <Badge colorScheme="green">Write</Badge> : null}
|
||||
{relayConfig.mode & RelayMode.READ ? <Badge>Read</Badge> : null}
|
||||
<IconButton
|
||||
icon={<GlobalIcon />}
|
||||
onClick={() => navigate("/global?relay=" + relayConfig.url)}
|
||||
size="sm"
|
||||
aria-label="Global Feed"
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
<IntersectionObserverProvider<string> callback={callback}>
|
||||
<VStack divider={<StackDivider />} py="2" align="stretch">
|
||||
{userRelays.map((relayConfig) => (
|
||||
<Relay url={relayConfig.url} reviews={getRelayReviews(relayConfig.url, reviews)} />
|
||||
))}
|
||||
</VStack>
|
||||
{otherReviews.length > 0 && (
|
||||
<>
|
||||
<Heading>Other Reviews</Heading>
|
||||
<Flex direction="column" gap="2" pb="8">
|
||||
{otherReviews.map((event) => (
|
||||
<RelayReviewNote key={event.id} event={event} />
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user