add relay review form

This commit is contained in:
hzrd149 2023-08-07 20:30:16 -05:00
parent 2d6ead0db4
commit d8b29b4df7
8 changed files with 222 additions and 91 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add relay review form

View File

@ -53,12 +53,12 @@ export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
<Button onClick={() => navigate("/map")} leftIcon={<MapIcon />}>
Map
</Button>
<Button onClick={() => navigate("/profile")} leftIcon={<ProfileIcon />}>
Profile
</Button>
<Button onClick={() => navigate("/relays")} leftIcon={<RelayIcon />}>
Relays
</Button>
<Button onClick={() => navigate("/profile")} leftIcon={<ProfileIcon />}>
Profile
</Button>
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
Settings
</Button>

View File

@ -13,7 +13,16 @@ import {
} from "@chakra-ui/react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
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 { UserLink } from "../user-link";
import AccountSwitcher from "./account-switcher";
@ -51,15 +60,18 @@ export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "childr
<Button onClick={() => navigate(`/search`)} leftIcon={<SearchIcon />}>
Search
</Button>
<Button onClick={() => navigate(`/profile`)} leftIcon={<ProfileIcon />}>
Profile
</Button>
<Button onClick={() => navigate("/streams")} leftIcon={<LiveStreamIcon />}>
Streams
</Button>
<Button onClick={() => navigate("/map")} leftIcon={<MapIcon />}>
Map
</Button>
<Button onClick={() => navigate("/relays")} leftIcon={<RelayIcon />}>
Relays
</Button>
<Button onClick={() => navigate(`/profile`)} leftIcon={<ProfileIcon />}>
Profile
</Button>
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
Settings
</Button>

View File

@ -1,17 +1,45 @@
import { Flex, IconProps } from "@chakra-ui/react";
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 renderStar = (i: number) => {
if (normalized >= i + 1) return <StarFullIcon {...props} />;
if (normalized === i + 0.5) return <StarHalfIcon {...props} />;
return <StarEmptyIcon {...props} />;
if (normalized >= i + 1) return <StarFullIcon key={i} color={color} {...props} />;
if (normalized === i + 0.5) return <StarHalfIcon key={i} color={color} {...props} />;
return <StarEmptyIcon key={i} color={color} {...props} />;
};
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)
.fill(0)
.map((_, i) => renderStar(i))}

View 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";

View 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>
);
}

View File

@ -1,86 +1,39 @@
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 { useRelayInfo } from "../../hooks/use-relay-info";
import { RelayDebugButton, RelayJoinAction, RelayMetadata } from "./components/relay-card";
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 RelayReviewNote from "./components/relay-review-note";
// 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>
);
}
import SupportedNIPs from "./components/supported-nips";
import { useForm } from "react-hook-form";
import StarRating from "../../components/star-rating";
import { DraftNostrEvent } from "../../types/nostr-event";
import { RELAY_REVIEW_LABEL, RELAY_REVIEW_LABEL_NAMESPACE, REVIEW_KIND } from "../../helpers/nostr/reviews";
import dayjs from "dayjs";
import { useSigningContext } from "../../providers/signing-provider";
import { nostrPostAction } from "../../classes/nostr-post-action";
function RelayReviews({ relay }: { relay: string }) {
const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader(`${relay}-reviews`, readRelays, {
kinds: [1985],
"#r": [relay],
"#l": ["review/relay"],
"#l": [RELAY_REVIEW_LABEL],
});
const events = useSubject(timeline.timeline);
@ -94,22 +47,68 @@ function RelayReviews({ relay }: { relay: string }) {
);
}
function RelayPage({ relay }: { relay: string }) {
const { info } = useRelayInfo(relay);
function RelayReviewForm({ onClose, relay }: { onClose: () => void; relay: string }) {
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 (
<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">
<Heading>{relay}</Heading>
<Heading isTruncated size={{ base: "md", sm: "lg" }}>
{relay}
</Heading>
<RelayDebugButton url={relay} ml="auto" />
<RelayJoinAction url={relay} />
</Flex>
<RelayMetadata url={relay} />
<Flex gap="2" wrap="wrap">
{info?.supported_nips?.map((nip) => (
<NipTag key={nip} nip={nip} />
))}
</Flex>
{info?.supported_nips && <SupportedNIPs nips={info?.supported_nips} />}
<Tabs display="flex" flexDirection="column" flexGrow="1" isLazy colorScheme="brand">
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
<Tab>Reviews</Tab>
@ -118,6 +117,13 @@ function RelayPage({ relay }: { relay: string }) {
<TabPanels>
<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} />
</TabPanel>
<TabPanel py="2" px="0"></TabPanel>

View File

@ -18,7 +18,7 @@ function Relay({ url, reviews }: { url: string; reviews: NostrEvent[] }) {
<Flex gap="2" alignItems="center">
<RelayFavicon relay={url} size="xs" />
<Heading size="md" isTruncated>
{url}
<RouterLink to={`/r/${encodeURIComponent(url)}`}>{url}</RouterLink>
</Heading>
<Spacer />
<RelayDebugButton url={url} size="sm" />