add simple nip19 tool

This commit is contained in:
hzrd149 2023-06-05 16:17:28 -04:00
parent 8e49f85dd2
commit d58ef29e25
9 changed files with 184 additions and 36 deletions

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add simple nip19 tool

10
cypress/e2e/profile.cy.ts Normal file

@ -0,0 +1,10 @@
describe("Profile view", () => {
it("should load user on single relay", () => {
cy.visit(
"/u/nprofile1qqsp6hxqjatvxtesgszs8aee0fcjccxa3ef3mzjva4uv2yr5lucp6jcpzemhxue69uhhyumnd3shjtnwdaehgu3wd4hk2s8c5un"
);
cy.contains("fjsmu");
cy.contains("https://rsshub.app/pixiv/user/7569500@rsslay.nostr.moe");
});
});

@ -34,6 +34,8 @@ import NostrLinkView from "./views/link";
import UserReportsTab from "./views/user/reports";
import appSettings from "./services/app-settings";
import UserMediaTab from "./views/user/media";
import { ToolsHomeView } from "./views/tools";
import { Nip19ToolsView } from "./views/tools/nip19";
// code split search view because QrScanner library is 400kB
const SearchView = React.lazy(() => import("./views/search"));
@ -85,6 +87,13 @@ const router = createBrowserRouter([
{ path: "dm", element: <DirectMessagesView /> },
{ path: "dm/:key", element: <DirectMessageChatView /> },
{ path: "profile", element: <ProfileView /> },
{
path: "tools",
children: [
{ path: "", element: <ToolsHomeView /> },
{ path: "nip19", element: <Nip19ToolsView /> },
],
},
{ path: "l/:link", element: <NostrLinkView /> },
{ path: "t/:hashtag", element: <HashTagView /> },
{

@ -241,3 +241,9 @@ export const GithubIcon = createIcon({
d: "M10 0a10 10 0 0 0-3.16 19.49c.5.1.68-.22.68-.48l-.01-1.7c-2.78.6-3.37-1.34-3.37-1.34-.46-1.16-1.11-1.47-1.11-1.47-.9-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.52 2.34 1.08 2.91.83.1-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.1.39-1.99 1.03-2.69a3.6 3.6 0 0 1 .1-2.64s.84-.27 2.75 1.02a9.58 9.58 0 0 1 5 0c1.91-1.3 2.75-1.02 2.75-1.02.55 1.37.2 2.4.1 2.64.64.7 1.03 1.6 1.03 2.69 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85l-.01 2.75c0 .26.18.58.69.48A10 10 0 0 0 10 0",
defaultProps,
});
export const ToolsIcon = createIcon({
displayName: "ToolsIcon",
d: "M5.32894 3.27158C6.56203 2.8332 7.99181 3.10749 8.97878 4.09446C10.0997 5.21537 10.3014 6.90741 9.58382 8.23385L20.2925 18.9437L18.8783 20.3579L8.16933 9.64875C6.84277 10.3669 5.1502 10.1654 4.02903 9.04421C3.04178 8.05696 2.76761 6.62665 3.20652 5.39332L5.44325 7.63C6.02903 8.21578 6.97878 8.21578 7.56457 7.63C8.15035 7.04421 8.15035 6.09446 7.56457 5.50868L5.32894 3.27158ZM15.6963 5.15512L18.8783 3.38736L20.2925 4.80157L18.5247 7.98355L16.757 8.3371L14.6356 10.4584L13.2214 9.04421L15.3427 6.92289L15.6963 5.15512ZM8.97878 13.2868L10.393 14.7011L5.08969 20.0044C4.69917 20.3949 4.066 20.3949 3.67548 20.0044C3.31285 19.6417 3.28695 19.0699 3.59777 18.6774L3.67548 18.5902L8.97878 13.2868Z",
defaultProps,
});

@ -1,10 +1,19 @@
import { SettingsIcon } from "@chakra-ui/icons";
import { Avatar, Button, Flex, Heading, LinkOverlay, Text, VStack } from "@chakra-ui/react";
import { Link, useNavigate } from "react-router-dom";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { useCurrentAccount } from "../../hooks/use-current-account";
import accountService from "../../services/account";
import { ConnectedRelays } from "../connected-relays";
import { ChatIcon, FeedIcon, LogoutIcon, NotificationIcon, ProfileIcon, RelayIcon, SearchIcon } from "../icons";
import {
ChatIcon,
FeedIcon,
LogoutIcon,
NotificationIcon,
ProfileIcon,
RelayIcon,
SearchIcon,
ToolsIcon,
} from "../icons";
import ProfileLink from "./profile-link";
import AccountSwitcher from "./account-switcher";
@ -15,7 +24,7 @@ export default function DesktopSideNav() {
return (
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
<Flex gap="2" alignItems="center" position="relative">
<LinkOverlay as={Link} to="/" />
<LinkOverlay as={RouterLink} to="/" />
<Avatar src="/apple-touch-icon.png" size="sm" />
<Heading size="md">noStrudel</Heading>
</Flex>

@ -1,3 +1,4 @@
import { forwardRef, useState } from "react";
import {
Badge,
Button,
@ -17,11 +18,9 @@ import {
ModalProps,
useDisclosure,
} from "@chakra-ui/react";
import { useState } from "react";
import { useAsync } from "react-use";
import { unique } from "../helpers/array";
import { RelayIcon, SearchIcon } from "./icons";
import { safeRelayUrl } from "../helpers/url";
function RelayPickerModal({
onSelect,
@ -82,32 +81,31 @@ function RelayPickerModal({
export type RelayUrlInputProps = Omit<InputProps, "type">;
export const RelayUrlInput = ({
onChange,
...props
}: Omit<RelayUrlInputProps, "onChange"> & { onChange: (url: string) => void }) => {
const { isOpen, onClose, onOpen } = useDisclosure();
const { value: relaysJson } = useAsync(async () =>
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise<string[]>)
);
const relaySuggestions = unique(relaysJson ?? []);
export const RelayUrlInput = forwardRef(
({ onChange, ...props }: Omit<RelayUrlInputProps, "onChange"> & { onChange: (url: string) => void }, ref) => {
const { isOpen, onClose, onOpen } = useDisclosure();
const { value: relaysJson } = useAsync(async () =>
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise<string[]>)
);
const relaySuggestions = unique(relaysJson ?? []);
return (
<>
<InputGroup>
<Input list="relay-suggestions" type="url" onChange={(e) => onChange(e.target.value)} {...props} />
<datalist id="relay-suggestions">
{relaySuggestions.map((url) => (
<option key={url} value={url}>
{url}
</option>
))}
</datalist>
<InputRightElement>
<IconButton icon={<RelayIcon />} aria-label="Pick from list" size="sm" onClick={onOpen} />
</InputRightElement>
</InputGroup>
<RelayPickerModal onClose={onClose} isOpen={isOpen} onSelect={(url) => onChange(url)} size="2xl" />
</>
);
};
return (
<>
<InputGroup>
<Input ref={ref} list="relay-suggestions" type="url" onChange={(e) => onChange(e.target.value)} {...props} />
<datalist id="relay-suggestions">
{relaySuggestions.map((url) => (
<option key={url} value={url}>
{url}
</option>
))}
</datalist>
<InputRightElement>
<IconButton icon={<RelayIcon />} aria-label="Pick from list" size="sm" onClick={onOpen} />
</InputRightElement>
</InputGroup>
<RelayPickerModal onClose={onClose} isOpen={isOpen} onSelect={(url) => onChange(url)} size="2xl" />
</>
);
}
);

@ -1,6 +1,7 @@
import { Button, Flex, Accordion, Link } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import accountService from "../../services/account";
import { GithubIcon, LogoutIcon } from "../../components/icons";
import { GithubIcon, LogoutIcon, ToolsIcon } from "../../components/icons";
import LightningSettings from "./lightning-settings";
import DatabaseSettings from "./database-settings";
import DisplaySettings from "./display-settings";
@ -18,11 +19,14 @@ export default function SettingsView() {
<DatabaseSettings />
</Accordion>
<Flex gap="2" padding="4" alignItems="center" justifyContent="space-between">
<Flex gap="2" padding="4" alignItems="center">
<Button leftIcon={<LogoutIcon />} onClick={() => accountService.logout()}>
Logout
</Button>
<Link isExternal href="https://github.com/hzrd149/nostrudel">
<Button as={RouterLink} to="/tools" leftIcon={<ToolsIcon />}>
Tools
</Button>
<Link isExternal href="https://github.com/hzrd149/nostrudel" ml="auto">
<GithubIcon /> Github
</Link>
</Flex>

18
src/views/tools/index.tsx Normal file

@ -0,0 +1,18 @@
import { Button, Flex, Heading } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { ToolsIcon } from "../../components/icons";
export function ToolsHomeView() {
return (
<Flex direction="column" gap="4" p="4">
<Heading>
<ToolsIcon /> Tools
</Heading>
<Flex wrap="wrap">
<Button as={RouterLink} to="./nip19">
Nip-19 encode/decode
</Button>
</Flex>
</Flex>
);
}

89
src/views/tools/nip19.tsx Normal file

@ -0,0 +1,89 @@
import {
Button,
Card,
CardBody,
Flex,
FormControl,
FormErrorMessage,
FormLabel,
Heading,
Input,
useToast,
} from "@chakra-ui/react";
import { ToolsIcon } from "../../components/icons";
import { useForm } from "react-hook-form";
import { RelayUrlInput } from "../../components/relay-url-input";
import { useState } from "react";
import { normalizeToHex } from "../../helpers/nip19";
import { nip19 } from "nostr-tools";
import { normalizeRelayUrl } from "../../helpers/url";
import RawValue from "../../components/debug-modals/raw-value";
function EncodeForm() {
const toast = useToast();
const { handleSubmit, register, formState, setValue } = useForm({
mode: "onBlur",
defaultValues: {
pubkey: "",
relay: "",
},
});
const [output, setOutput] = useState("");
const convert = handleSubmit((values) => {
try {
const pubkey = normalizeToHex(values.pubkey);
if (!pubkey) throw new Error("bad pubkey");
const relay = normalizeRelayUrl(values.relay);
const nprofile = nip19.nprofileEncode({
pubkey,
relays: [relay],
});
setOutput(nprofile);
} catch (e) {
if (e instanceof Error) {
toast({ description: e.message });
}
}
});
return (
<Card>
<CardBody>
<form onSubmit={convert}>
<FormControl isInvalid={!!formState.errors.pubkey}>
<FormLabel>Public key</FormLabel>
<Input {...register("pubkey", { minLength: 8 })} placeholder="npub or hex" />
{formState.errors.pubkey && <FormErrorMessage>{formState.errors.pubkey.message}</FormErrorMessage>}
</FormControl>
<FormControl isInvalid={!!formState.errors.pubkey}>
<FormLabel>Relay url</FormLabel>
<RelayUrlInput
{...register("relay")}
onChange={(v) => setValue("relay", v)}
placeholder="wss://relay.example.com"
/>
{formState.errors.pubkey && <FormErrorMessage>{formState.errors.pubkey.message}</FormErrorMessage>}
<Button type="submit">Encode</Button>
</FormControl>
</form>
{output && <RawValue heading="nprofile" value={output} />}
</CardBody>
</Card>
);
}
export function Nip19ToolsView() {
return (
<Flex direction="column" gap="4" p="4">
<Heading>
<ToolsIcon /> Nip-19 Tools
</Heading>
<Heading size="sm">Encode</Heading>
<EncodeForm />
</Flex>
);
}