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 />}> <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>

View File

@@ -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>

View File

@@ -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))}

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 { 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>

View File

@@ -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" />