mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-28 20:43:33 +02:00
add relay review form
This commit is contained in:
5
.changeset/six-games-beg.md
Normal file
5
.changeset/six-games-beg.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add relay review form
|
@@ -53,12 +53,12 @@ export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
|
|||||||
<Button onClick={() => navigate("/map")} leftIcon={<MapIcon />}>
|
<Button onClick={() => navigate("/map")} leftIcon={<MapIcon />}>
|
||||||
Map
|
Map
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => navigate("/profile")} leftIcon={<ProfileIcon />}>
|
|
||||||
Profile
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => navigate("/relays")} leftIcon={<RelayIcon />}>
|
<Button onClick={() => navigate("/relays")} leftIcon={<RelayIcon />}>
|
||||||
Relays
|
Relays
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={() => navigate("/profile")} leftIcon={<ProfileIcon />}>
|
||||||
|
Profile
|
||||||
|
</Button>
|
||||||
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
|
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
|
||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -13,7 +13,16 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||||
import { ConnectedRelays } from "../connected-relays";
|
import { ConnectedRelays } from "../connected-relays";
|
||||||
import { HomeIcon, LiveStreamIcon, LogoutIcon, ProfileIcon, RelayIcon, SearchIcon, SettingsIcon } from "../icons";
|
import {
|
||||||
|
HomeIcon,
|
||||||
|
LiveStreamIcon,
|
||||||
|
LogoutIcon,
|
||||||
|
MapIcon,
|
||||||
|
ProfileIcon,
|
||||||
|
RelayIcon,
|
||||||
|
SearchIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
} from "../icons";
|
||||||
import { UserAvatar } from "../user-avatar";
|
import { UserAvatar } from "../user-avatar";
|
||||||
import { UserLink } from "../user-link";
|
import { UserLink } from "../user-link";
|
||||||
import AccountSwitcher from "./account-switcher";
|
import AccountSwitcher from "./account-switcher";
|
||||||
@@ -51,15 +60,18 @@ export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "childr
|
|||||||
<Button onClick={() => navigate(`/search`)} leftIcon={<SearchIcon />}>
|
<Button onClick={() => navigate(`/search`)} leftIcon={<SearchIcon />}>
|
||||||
Search
|
Search
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => navigate(`/profile`)} leftIcon={<ProfileIcon />}>
|
|
||||||
Profile
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => navigate("/streams")} leftIcon={<LiveStreamIcon />}>
|
<Button onClick={() => navigate("/streams")} leftIcon={<LiveStreamIcon />}>
|
||||||
Streams
|
Streams
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={() => navigate("/map")} leftIcon={<MapIcon />}>
|
||||||
|
Map
|
||||||
|
</Button>
|
||||||
<Button onClick={() => navigate("/relays")} leftIcon={<RelayIcon />}>
|
<Button onClick={() => navigate("/relays")} leftIcon={<RelayIcon />}>
|
||||||
Relays
|
Relays
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={() => navigate(`/profile`)} leftIcon={<ProfileIcon />}>
|
||||||
|
Profile
|
||||||
|
</Button>
|
||||||
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
|
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
|
||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -1,17 +1,45 @@
|
|||||||
import { Flex, IconProps } from "@chakra-ui/react";
|
import { Flex, IconProps } from "@chakra-ui/react";
|
||||||
import { StarEmptyIcon, StarFullIcon, StarHalfIcon } from "./icons";
|
import { StarEmptyIcon, StarFullIcon, StarHalfIcon } from "./icons";
|
||||||
|
import styled from "@emotion/styled";
|
||||||
|
|
||||||
export default function StarRating({ quality, stars = 5, ...props }: { quality: number; stars?: number } & IconProps) {
|
const HiddenSlider = styled.input`
|
||||||
|
position: absolute;
|
||||||
|
left: -0.5em;
|
||||||
|
right: -0.5em;
|
||||||
|
bottom: 0;
|
||||||
|
top: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: -moz-available;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function StarRating({
|
||||||
|
quality,
|
||||||
|
stars = 5,
|
||||||
|
color = "yellow.300",
|
||||||
|
onChange,
|
||||||
|
...props
|
||||||
|
}: { quality: number; stars?: number; onChange?: (quality: number) => void } & Omit<IconProps, "onChange">) {
|
||||||
const normalized = Math.round(quality * (stars * 2)) / 2;
|
const normalized = Math.round(quality * (stars * 2)) / 2;
|
||||||
|
|
||||||
const renderStar = (i: number) => {
|
const renderStar = (i: number) => {
|
||||||
if (normalized >= i + 1) return <StarFullIcon {...props} />;
|
if (normalized >= i + 1) return <StarFullIcon key={i} color={color} {...props} />;
|
||||||
if (normalized === i + 0.5) return <StarHalfIcon {...props} />;
|
if (normalized === i + 0.5) return <StarHalfIcon key={i} color={color} {...props} />;
|
||||||
return <StarEmptyIcon {...props} />;
|
return <StarEmptyIcon key={i} color={color} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap="1">
|
<Flex gap="1" position="relative">
|
||||||
|
{onChange && (
|
||||||
|
<HiddenSlider
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={1 / 10}
|
||||||
|
onChange={(e) => onChange(parseFloat(e.target.value))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{Array(stars)
|
{Array(stars)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, i) => renderStar(i))}
|
.map((_, i) => renderStar(i))}
|
||||||
|
3
src/helpers/nostr/reviews.ts
Normal file
3
src/helpers/nostr/reviews.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const REVIEW_KIND = 1985;
|
||||||
|
export const RELAY_REVIEW_LABEL = "review/relay";
|
||||||
|
export const RELAY_REVIEW_LABEL_NAMESPACE = "social.coracle.ontology";
|
77
src/views/relays/components/supported-nips.tsx
Normal file
77
src/views/relays/components/supported-nips.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { Flex, Tag, Tooltip } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
// copied from github
|
||||||
|
const NIP_NAMES: Record<string, string> = {
|
||||||
|
"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 (
|
||||||
|
<Tooltip label={NIP_NAMES[nipStr]}>
|
||||||
|
<Tag as="a" target="_blank" href={`https://github.com/nostr-protocol/nips/blob/master/${nipStr}.md`}>
|
||||||
|
NIP-{nip}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SupportedNIPs({ nips }: { nips: number[] }) {
|
||||||
|
return (
|
||||||
|
<Flex gap="2" wrap="wrap">
|
||||||
|
{nips.map((nip) => (
|
||||||
|
<NipTag key={nip} nip={nip} />
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,86 +1,39 @@
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { Box, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs, Tag, Tooltip } from "@chakra-ui/react";
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
TabPanels,
|
||||||
|
Tabs,
|
||||||
|
Textarea,
|
||||||
|
useDisclosure,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
import { safeRelayUrl } from "../../helpers/url";
|
import { safeRelayUrl } from "../../helpers/url";
|
||||||
import { useRelayInfo } from "../../hooks/use-relay-info";
|
import { useRelayInfo } from "../../hooks/use-relay-info";
|
||||||
import { RelayDebugButton, RelayJoinAction, RelayMetadata } from "./components/relay-card";
|
import { RelayDebugButton, RelayJoinAction, RelayMetadata } from "./components/relay-card";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls, useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||||
import RelayReviewNote from "./components/relay-review-note";
|
import RelayReviewNote from "./components/relay-review-note";
|
||||||
|
import SupportedNIPs from "./components/supported-nips";
|
||||||
// copied from github
|
import { useForm } from "react-hook-form";
|
||||||
const NIP_NAMES: Record<string, string> = {
|
import StarRating from "../../components/star-rating";
|
||||||
"01": "Basic protocol",
|
import { DraftNostrEvent } from "../../types/nostr-event";
|
||||||
"02": "Contact List and Petnames",
|
import { RELAY_REVIEW_LABEL, RELAY_REVIEW_LABEL_NAMESPACE, REVIEW_KIND } from "../../helpers/nostr/reviews";
|
||||||
"03": "OpenTimestamps Attestations for Events",
|
import dayjs from "dayjs";
|
||||||
"04": "Encrypted Direct Message",
|
import { useSigningContext } from "../../providers/signing-provider";
|
||||||
"05": "Mapping Nostr keys to DNS-based internet identifiers",
|
import { nostrPostAction } from "../../classes/nostr-post-action";
|
||||||
"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 (
|
|
||||||
<Tooltip label={NIP_NAMES[nipStr]}>
|
|
||||||
<Tag as="a" target="_blank" href={`https://github.com/nostr-protocol/nips/blob/master/${nipStr}.md`}>
|
|
||||||
NIP-{nip}
|
|
||||||
</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RelayReviews({ relay }: { relay: string }) {
|
function RelayReviews({ relay }: { relay: string }) {
|
||||||
const readRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls();
|
||||||
const timeline = useTimelineLoader(`${relay}-reviews`, readRelays, {
|
const timeline = useTimelineLoader(`${relay}-reviews`, readRelays, {
|
||||||
kinds: [1985],
|
kinds: [1985],
|
||||||
"#r": [relay],
|
"#r": [relay],
|
||||||
"#l": ["review/relay"],
|
"#l": [RELAY_REVIEW_LABEL],
|
||||||
});
|
});
|
||||||
|
|
||||||
const events = useSubject(timeline.timeline);
|
const events = useSubject(timeline.timeline);
|
||||||
@@ -94,22 +47,68 @@ function RelayReviews({ relay }: { relay: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RelayPage({ relay }: { relay: string }) {
|
function RelayReviewForm({ onClose, relay }: { onClose: () => void; relay: string }) {
|
||||||
const { info } = useRelayInfo(relay);
|
const { requestSignature } = useSigningContext();
|
||||||
|
const writeRelays = useWriteRelayUrls();
|
||||||
|
const { register, getValues, watch, handleSubmit, setValue } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
quality: 0.6,
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
watch("quality");
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (values) => {
|
||||||
|
const draft: DraftNostrEvent = {
|
||||||
|
kind: REVIEW_KIND,
|
||||||
|
content: values.content,
|
||||||
|
tags: [
|
||||||
|
["l", RELAY_REVIEW_LABEL, new URL(relay).host, JSON.stringify({ quality: values.quality })],
|
||||||
|
["L", RELAY_REVIEW_LABEL_NAMESPACE],
|
||||||
|
["r", relay],
|
||||||
|
],
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const signed = await requestSignature(draft);
|
||||||
|
if (!signed) return;
|
||||||
|
nostrPostAction(writeRelays, signed);
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" alignItems="stretch" gap="2" py="2">
|
<Flex as="form" direction="column" onSubmit={onSubmit} gap="2" mb="2">
|
||||||
|
<Flex gap="2">
|
||||||
|
<Heading size="md">Write review</Heading>
|
||||||
|
<StarRating quality={getValues().quality} fontSize="1.5rem" onChange={(q) => setValue("quality", q)} />
|
||||||
|
</Flex>
|
||||||
|
<Textarea {...register("content")} rows={5} placeholder="A short description of your experience with the relay" />
|
||||||
|
<Flex gap="2" ml="auto">
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
<Button type="submit" colorScheme="brand">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RelayPage({ relay }: { relay: string }) {
|
||||||
|
const { info } = useRelayInfo(relay);
|
||||||
|
const showReviewForm = useDisclosure();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" alignItems="stretch" gap="2" p="2">
|
||||||
<Flex gap="2" alignItems="center">
|
<Flex gap="2" alignItems="center">
|
||||||
<Heading>{relay}</Heading>
|
<Heading isTruncated size={{ base: "md", sm: "lg" }}>
|
||||||
|
{relay}
|
||||||
|
</Heading>
|
||||||
<RelayDebugButton url={relay} ml="auto" />
|
<RelayDebugButton url={relay} ml="auto" />
|
||||||
<RelayJoinAction url={relay} />
|
<RelayJoinAction url={relay} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<RelayMetadata url={relay} />
|
<RelayMetadata url={relay} />
|
||||||
<Flex gap="2" wrap="wrap">
|
{info?.supported_nips && <SupportedNIPs nips={info?.supported_nips} />}
|
||||||
{info?.supported_nips?.map((nip) => (
|
|
||||||
<NipTag key={nip} nip={nip} />
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
<Tabs display="flex" flexDirection="column" flexGrow="1" isLazy colorScheme="brand">
|
<Tabs display="flex" flexDirection="column" flexGrow="1" isLazy colorScheme="brand">
|
||||||
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
|
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
|
||||||
<Tab>Reviews</Tab>
|
<Tab>Reviews</Tab>
|
||||||
@@ -118,6 +117,13 @@ function RelayPage({ relay }: { relay: string }) {
|
|||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel py="2" px="0">
|
<TabPanel py="2" px="0">
|
||||||
|
{showReviewForm.isOpen ? (
|
||||||
|
<RelayReviewForm onClose={showReviewForm.onClose} relay={relay} />
|
||||||
|
) : (
|
||||||
|
<Button colorScheme="brand" ml="aut" mb="2" onClick={showReviewForm.onOpen}>
|
||||||
|
Write review
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<RelayReviews relay={relay} />
|
<RelayReviews relay={relay} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel py="2" px="0"></TabPanel>
|
<TabPanel py="2" px="0"></TabPanel>
|
||||||
|
@@ -18,7 +18,7 @@ function Relay({ url, reviews }: { url: string; reviews: NostrEvent[] }) {
|
|||||||
<Flex gap="2" alignItems="center">
|
<Flex gap="2" alignItems="center">
|
||||||
<RelayFavicon relay={url} size="xs" />
|
<RelayFavicon relay={url} size="xs" />
|
||||||
<Heading size="md" isTruncated>
|
<Heading size="md" isTruncated>
|
||||||
{url}
|
<RouterLink to={`/r/${encodeURIComponent(url)}`}>{url}</RouterLink>
|
||||||
</Heading>
|
</Heading>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<RelayDebugButton url={url} size="sm" />
|
<RelayDebugButton url={url} size="sm" />
|
||||||
|
Reference in New Issue
Block a user