cleanup debug modal

This commit is contained in:
hzrd149 2024-11-19 11:33:32 -06:00
parent dbd724a118
commit bf6d243d61
16 changed files with 413 additions and 196 deletions

82
pnpm-lock.yaml generated
View File

@ -92,25 +92,25 @@ importers:
version: 4.9.2(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
applesauce-channel:
specifier: next
version: 0.0.0-next-20241119142128(typescript@5.6.3)
version: 0.0.0-next-20241119173145(typescript@5.6.3)
applesauce-content:
specifier: next
version: 0.0.0-next-20241119142128(typescript@5.6.3)
version: 0.0.0-next-20241119173145(typescript@5.6.3)
applesauce-core:
specifier: next
version: 0.0.0-next-20241119142128(typescript@5.6.3)
version: 0.0.0-next-20241119173145(typescript@5.6.3)
applesauce-lists:
specifier: next
version: 0.0.0-next-20241119142128(typescript@5.6.3)
version: 0.0.0-next-20241119173145(typescript@5.6.3)
applesauce-net:
specifier: next
version: 0.0.0-next-20241119142128(typescript@5.6.3)
version: 0.0.0-next-20241119173145(typescript@5.6.3)
applesauce-react:
specifier: next
version: 0.0.0-next-20241119142128(typescript@5.6.3)
version: 0.0.0-next-20241119173145(typescript@5.6.3)
applesauce-signer:
specifier: next
version: 0.0.0-next-20241119142128(typescript@5.6.3)
version: 0.0.0-next-20241119173145(typescript@5.6.3)
bech32:
specifier: ^2.0.0
version: 2.0.0
@ -1869,26 +1869,26 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
applesauce-channel@0.0.0-next-20241119142128:
resolution: {integrity: sha512-AynaT+pM9fNBValeuj3ad1/KNEcLyGo2oupnXYvvY1G0cgVSW6L3YAO+p5VA/XCrWGsecnGQksJYVAvLAGqDHQ==}
applesauce-channel@0.0.0-next-20241119173145:
resolution: {integrity: sha512-+2bysV0gNdC2AKrylO+xP8ETp2eGbPbw7lz1kHI2p+fA5OIFn0j+6Ge8wEGv4E9n9eLIxa6welZKqWolCR0xDg==}
applesauce-content@0.0.0-next-20241119142128:
resolution: {integrity: sha512-e6oPmu6fDRFzfra9WpxffytgFE4o5Ot5LgCE8uZNTUZYY3/f05xW4qPJAIuIddP3RK19AvsXsXHDHotQ+KV07g==}
applesauce-content@0.0.0-next-20241119173145:
resolution: {integrity: sha512-JSjnM2rRCEhsDH0PzsfN4iFetOoE1UVY13ug2dsYLYM7K/ZZDgCj1W7wtIFZQKQANrqUs0Cx4zdhTTnSnbdDmQ==}
applesauce-core@0.0.0-next-20241119142128:
resolution: {integrity: sha512-mftudp86ZKQYH+eFThOUj7yt+gFgMlFdLBW7VEHJAnVlXj1FLPQohvf6C0kpFtMNxnAPMlRCPGLUQn5z32VBfA==}
applesauce-core@0.0.0-next-20241119173145:
resolution: {integrity: sha512-Y0w+uc+ByojiyrFgQInY2Klg51sYnTWPXvWRdT7RP1rw4WkOTvLvek1y6VoUmrcZG7Ss0f/eKiQiqhWamNIxDQ==}
applesauce-lists@0.0.0-next-20241119142128:
resolution: {integrity: sha512-QUrorAGEcj7b0jIarVhUWs32VlRc2ovpE6GOxa9rxHln8aRsJ2VEtKKTxc+dpMhnG5hB1v4MT9LXnvSnulw3mA==}
applesauce-lists@0.0.0-next-20241119173145:
resolution: {integrity: sha512-XzJtS5ZNnIE3YUh3eppilyGIpfnE1RqV4VuQqoNGhGYwbbn2b3o8Z0+dKe7+VyGu50HGcCAlhvdTjgV6VgMiXw==}
applesauce-net@0.0.0-next-20241119142128:
resolution: {integrity: sha512-/vsNdP+icA/K4So8ilqh5YMu77bXQ5F4kuV0WZai5BAeIW+sbvPju+7jWUTs/AFSNFDLLL8jzx7FzXe2Ui4tuA==}
applesauce-net@0.0.0-next-20241119173145:
resolution: {integrity: sha512-TUdIY+2Ida1gMg4KTcPoByRdw9uLd3bl8ztyS51adfeKow0Qs7bAhuBLuzLav0sUwG8QzWVGqBHgVOf/KX7uTw==}
applesauce-react@0.0.0-next-20241119142128:
resolution: {integrity: sha512-FJzgri22ZLDHp8imsnvGDKu3k0LhXewolElyCyJZnNP20tlsjHjwGrnScaNTYFtnsbDwRx3UhF9NiYsiWwZS1g==}
applesauce-react@0.0.0-next-20241119173145:
resolution: {integrity: sha512-uMyAPFJVxT8/jPatxw/X/HmVSSXCyXyDg7vXpexr+aejryidIXIUDI2aZnaHgNbknx8QsqeeXP2d5AgvCrDnZA==}
applesauce-signer@0.0.0-next-20241119142128:
resolution: {integrity: sha512-/hL/rSzNR4Y6UQ58/d8MOzOXccwHbR1DIkHOvn+Dir3ZYs5qDXFsSwhC4zNmmHab7Vo7aVgmCpE6TKzY026u6w==}
applesauce-signer@0.0.0-next-20241119173145:
resolution: {integrity: sha512-uLqsdLinVtwaNp1YfNMdK8ohPO/GxLK2F+srWYFUeKFErY08bXp8cbQubuaD9FYVaUOOWERFrqwoGv+FEb5Msw==}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
@ -3124,8 +3124,8 @@ packages:
micromark-util-sanitize-uri@2.0.1:
resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
micromark-util-subtokenize@2.0.2:
resolution: {integrity: sha512-xKxhkB62vwHUuuxHe9Xqty3UaAsizV2YKq5OV344u3hFBbf8zIYrhYOWhAQb94MtMPkjTOzzjJ/hid9/dR5vFA==}
micromark-util-subtokenize@2.0.3:
resolution: {integrity: sha512-VXJJuNxYWSoYL6AJ6OQECCFGhIU2GGHMw8tahogePBrjkG8aCCas3ibkp7RnVOSTClg2is05/R7maAhF1XyQMg==}
micromark-util-symbol@2.0.1:
resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
@ -6158,22 +6158,22 @@ snapshots:
dependencies:
color-convert: 2.0.1
applesauce-channel@0.0.0-next-20241119142128(typescript@5.6.3):
applesauce-channel@0.0.0-next-20241119173145(typescript@5.6.3):
dependencies:
applesauce-core: 0.0.0-next-20241119142128(typescript@5.6.3)
applesauce-core: 0.0.0-next-20241119173145(typescript@5.6.3)
nostr-tools: 2.10.3(typescript@5.6.3)
rxjs: 7.8.1
transitivePeerDependencies:
- supports-color
- typescript
applesauce-content@0.0.0-next-20241119142128(typescript@5.6.3):
applesauce-content@0.0.0-next-20241119173145(typescript@5.6.3):
dependencies:
'@cashu/cashu-ts': 2.0.0-rc1
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@types/unist': 3.0.3
applesauce-core: 0.0.0-next-20241119142128(typescript@5.6.3)
applesauce-core: 0.0.0-next-20241119173145(typescript@5.6.3)
mdast-util-find-and-replace: 3.0.1
nostr-tools: 2.10.3(typescript@5.6.3)
remark: 15.0.1
@ -6184,7 +6184,7 @@ snapshots:
- supports-color
- typescript
applesauce-core@0.0.0-next-20241119142128(typescript@5.6.3):
applesauce-core@0.0.0-next-20241119173145(typescript@5.6.3):
dependencies:
debug: 4.3.7
json-stringify-deterministic: 1.0.12
@ -6196,13 +6196,13 @@ snapshots:
- supports-color
- typescript
applesauce-lists@0.0.0-next-20241119142128(typescript@5.6.3):
applesauce-lists@0.0.0-next-20241119173145(typescript@5.6.3):
dependencies:
'@noble/hashes': 1.5.0
'@noble/secp256k1': 1.7.1
'@scure/base': 1.1.9
'@types/dom-serial': 1.0.6
applesauce-core: 0.0.0-next-20241119142128(typescript@5.6.3)
applesauce-core: 0.0.0-next-20241119173145(typescript@5.6.3)
debug: 4.3.7
nostr-tools: 2.10.3(typescript@5.6.3)
rxjs: 7.8.1
@ -6210,9 +6210,9 @@ snapshots:
- supports-color
- typescript
applesauce-net@0.0.0-next-20241119142128(typescript@5.6.3):
applesauce-net@0.0.0-next-20241119173145(typescript@5.6.3):
dependencies:
applesauce-core: 0.0.0-next-20241119142128(typescript@5.6.3)
applesauce-core: 0.0.0-next-20241119173145(typescript@5.6.3)
nanoid: 5.0.8
nostr-tools: 2.10.3(typescript@5.6.3)
rxjs: 7.8.1
@ -6221,10 +6221,10 @@ snapshots:
- supports-color
- typescript
applesauce-react@0.0.0-next-20241119142128(typescript@5.6.3):
applesauce-react@0.0.0-next-20241119173145(typescript@5.6.3):
dependencies:
applesauce-content: 0.0.0-next-20241119142128(typescript@5.6.3)
applesauce-core: 0.0.0-next-20241119142128(typescript@5.6.3)
applesauce-content: 0.0.0-next-20241119173145(typescript@5.6.3)
applesauce-core: 0.0.0-next-20241119173145(typescript@5.6.3)
nostr-tools: 2.10.3(typescript@5.6.3)
react: 18.3.1
rxjs: 7.8.1
@ -6232,14 +6232,14 @@ snapshots:
- supports-color
- typescript
applesauce-signer@0.0.0-next-20241119142128(typescript@5.6.3):
applesauce-signer@0.0.0-next-20241119173145(typescript@5.6.3):
dependencies:
'@noble/hashes': 1.5.0
'@noble/secp256k1': 1.7.1
'@scure/base': 1.1.9
'@types/dom-serial': 1.0.6
applesauce-core: 0.0.0-next-20241119142128(typescript@5.6.3)
applesauce-net: 0.0.0-next-20241119142128(typescript@5.6.3)
applesauce-core: 0.0.0-next-20241119173145(typescript@5.6.3)
applesauce-net: 0.0.0-next-20241119173145(typescript@5.6.3)
debug: 4.3.7
nanoid: 5.0.8
nostr-tools: 2.10.3(typescript@5.6.3)
@ -7641,7 +7641,7 @@ snapshots:
micromark-util-html-tag-name: 2.0.1
micromark-util-normalize-identifier: 2.0.1
micromark-util-resolve-all: 2.0.1
micromark-util-subtokenize: 2.0.2
micromark-util-subtokenize: 2.0.3
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.1
@ -7788,7 +7788,7 @@ snapshots:
micromark-util-encode: 2.0.1
micromark-util-symbol: 2.0.1
micromark-util-subtokenize@2.0.2:
micromark-util-subtokenize@2.0.3:
dependencies:
devlop: 1.1.0
micromark-util-chunked: 2.0.1
@ -7815,7 +7815,7 @@ snapshots:
micromark-util-normalize-identifier: 2.0.1
micromark-util-resolve-all: 2.0.1
micromark-util-sanitize-uri: 2.0.1
micromark-util-subtokenize: 2.0.2
micromark-util-subtokenize: 2.0.3
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.1
transitivePeerDependencies:

View File

@ -104,6 +104,7 @@ import PostSettings from "./views/settings/post";
import AccountSettings from "./views/settings/accounts";
import ArticlesHomeView from "./views/articles";
import ArticleView from "./views/articles/article";
import WalletView from "./views/wallet";
const TracksView = lazy(() => import("./views/tracks"));
const UserTracksTab = lazy(() => import("./views/user/tracks"));
const UserVideosTab = lazy(() => import("./views/user/videos"));
@ -336,6 +337,19 @@ const router = createHashRouter([
{ path: "", element: <NotificationsView /> },
],
},
{
path: "wallet",
children: [
{
path: "",
element: (
<RequireCurrentAccount>
<WalletView />
</RequireCurrentAccount>
),
},
],
},
{
path: "videos",
children: [

View File

@ -1,5 +1,5 @@
import { useState } from "react";
import { IconButton, IconButtonProps, useToast } from "@chakra-ui/react";
import { Button, ButtonProps, IconButton, IconButtonProps, useToast } from "@chakra-ui/react";
import { CheckIcon, CopyToClipboardIcon } from "./icons";
@ -8,7 +8,7 @@ type CopyIconButtonProps = Omit<IconButtonProps, "icon" | "value"> & {
icon?: IconButtonProps["icon"];
};
export const CopyIconButton = ({ value, icon, ...props }: CopyIconButtonProps) => {
export function CopyIconButton({ value, icon, ...props }: CopyIconButtonProps) {
const toast = useToast();
const [copied, setCopied] = useState(false);
@ -27,4 +27,31 @@ export const CopyIconButton = ({ value, icon, ...props }: CopyIconButtonProps) =
{...props}
/>
);
}
type CopyButtonProps = Omit<ButtonProps, "icon" | "value"> & {
value: string | undefined | (() => string);
icon?: IconButtonProps["icon"];
};
export function CopyButton({ value, icon, children, ...props }: CopyButtonProps) {
const toast = useToast();
const [copied, setCopied] = useState(false);
return (
<Button
leftIcon={copied ? <CheckIcon boxSize="1.5em" /> : icon || <CopyToClipboardIcon boxSize="1.2em" />}
onClick={() => {
const v: string | undefined = typeof value === "function" ? value() : value;
if (v && navigator.clipboard && !copied) {
navigator.clipboard.writeText(v);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} else toast({ description: v, isClosable: true, duration: null });
}}
{...props}
>
{copied ? "Copied" : children}
</Button>
);
}

View File

@ -1,173 +1,102 @@
import { PropsWithChildren, ReactNode, useCallback, useMemo, useState } from "react";
import { ComponentType, useState } from "react";
import {
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalCloseButton,
Heading,
AccordionItem,
Accordion,
AccordionPanel,
AccordionIcon,
AccordionButton,
Box,
ModalHeader,
Code,
AccordionPanelProps,
Button,
Text,
ComponentWithAs,
IconProps,
IconButton,
Flex,
} from "@chakra-ui/react";
import { nip19 } from "nostr-tools";
import { Link as RouterLink } from "react-router-dom";
import { ModalProps } from "@chakra-ui/react";
import { getSeenRelays } from "applesauce-core/helpers";
import { TextNoteContentSymbol } from "applesauce-content/text";
import { Root } from "applesauce-content/nast";
import { getContentPointers, getContentTagRefs, getThreadReferences } from "../../helpers/nostr/event";
import { NostrEvent } from "../../types/nostr-event";
import RawValue from "./raw-value";
import { CopyIconButton } from "../copy-icon-button";
import DebugEventTags from "./event-tags";
import { getSharableEventAddress } from "../../services/event-relay-hint";
import { usePublishEvent } from "../../providers/global/publish-provider";
import { EditIcon } from "../icons";
import { RelayFavicon } from "../relay-favicon";
import { ErrorBoundary } from "../error-boundary";
import { CodeIcon, RelayIcon, ThreadIcon } from "../icons";
import RawJsonPage from "./pages/raw";
import PenTool01 from "../icons/pen-tool-01";
import DebugContentPage from "./pages/content";
import DebugThreadingPage from "./pages/threading";
import Tag01 from "../icons/tag-01";
import DebugTagsPage from "./pages/tags";
import DebugEventRelaysPage from "./pages/relays";
import Database01 from "../icons/database-01";
import DebugEventCachePage from "./pages/cache";
function Section({
label,
children,
actions,
...props
}: PropsWithChildren<{ label: string; actions?: ReactNode }> & Omit<AccordionPanelProps, "children">) {
return (
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
{label}
</Box>
{actions && <div onClick={(e) => e.stopPropagation()}>{actions}</div>}
<AccordionIcon ml="2" />
</AccordionButton>
</h2>
<AccordionPanel display="flex" flexDirection="column" gap="2" alignItems="flex-start" {...props}>
{children}
</AccordionPanel>
</AccordionItem>
);
}
type DebugTool = {
id: string;
name: string;
icon: ComponentWithAs<"svg", IconProps>;
component: ComponentType<{ event: NostrEvent }>;
};
function JsonCode({ data }: { data: any }) {
const tools: DebugTool[] = [
{ id: "content", name: "Content", icon: PenTool01, component: DebugContentPage },
{ id: "json", name: "JSON", icon: CodeIcon, component: RawJsonPage },
{ id: "threading", name: "Threading", icon: ThreadIcon, component: DebugThreadingPage },
{ id: "tags", name: "Tags", icon: Tag01, component: DebugTagsPage },
{ id: "relays", name: "Relays", icon: RelayIcon, component: DebugEventRelaysPage },
{ id: "cache", name: "Cache", icon: Database01, component: DebugEventCachePage },
];
function DefaultPage({ event, setSelected }: { setSelected: (id: string) => void; event: NostrEvent }) {
return (
<Code whiteSpace="pre" overflowX="auto" width="100%" p="4">
{JSON.stringify(data, null, 2)}
</Code>
<>
<RawValue heading="Event Id" value={event.id} />
<RawValue heading="NIP-19 Encoded Id" value={nip19.noteEncode(event.id)} />
<RawValue heading="NIP-19 Pointer" value={getSharableEventAddress(event)} />
<Flex gap="2" flexWrap="wrap">
{tools.map(({ icon: Icon, name, id }) => (
<Button
variant="outline"
key={id}
leftIcon={<Icon boxSize={10} mb="4" />}
onClick={() => setSelected(id)}
h="36"
w="36"
flexDirection="column"
>
{name}
</Button>
))}
</Flex>
</>
);
}
export default function EventDebugModal({ event, ...props }: { event: NostrEvent } & Omit<ModalProps, "children">) {
const contentRefs = useMemo(() => getContentPointers(event.content), [event]);
const publish = usePublishEvent();
const [loading, setLoading] = useState(false);
const broadcast = useCallback(async () => {
setLoading(true);
await publish("Broadcast", event);
setLoading(false);
}, []);
const [selected, setSelected] = useState("");
const nast = Reflect.get(event, TextNoteContentSymbol) as Root;
const tool = tools.find((t) => t.id === selected);
const Page = tool?.component;
const IconComponent = tool?.icon;
return (
<Modal size="6xl" {...props}>
<ModalOverlay />
<ModalContent>
<ModalHeader p="4">{event.id}</ModalHeader>
<ModalHeader px="4" pt="4" pb="2" display="flex" alignItems="center" gap="2">
{tool && IconComponent && (
<IconButton icon={<IconComponent boxSize={6} />} aria-label="Select Tool" onClick={() => setSelected("")} />
)}
<Text as="span">{tool?.name || event.id}</Text>
</ModalHeader>
<ModalCloseButton />
<ModalBody p="0">
<Accordion allowToggle defaultIndex={event.content ? 1 : 2}>
<Section label="IDs">
<RawValue heading="Event Id" value={event.id} />
<RawValue heading="NIP-19 Encoded Id" value={nip19.noteEncode(event.id)} />
<RawValue heading="NIP-19 Pointer" value={getSharableEventAddress(event)} />
</Section>
<ModalBody px="4" pt="0" pb="4" display="flex" flexDirection="column" gap="2">
{Page ? <Page event={event} /> : <DefaultPage setSelected={setSelected} event={event} />}
<Section
label="Content"
p="0"
actions={<CopyIconButton aria-label="copy json" value={event.content} size="sm" />}
>
<Code whiteSpace="pre" overflowX="auto" width="100%" p="4">
{event.content}
</Code>
{contentRefs.length > 0 && (
<>
<Heading size="md" px="2">
embeds
</Heading>
{contentRefs.map((pointer, i) => (
<>
<Code whiteSpace="pre" overflowX="auto" width="100%" p="4">
{pointer.type + "\n"}
{JSON.stringify(pointer.data, null, 2)}
</Code>
</>
))}
</>
)}
</Section>
<Section
label="JSON"
p="0"
actions={
<>
<Button
leftIcon={<EditIcon />}
as={RouterLink}
to="/tools/publisher"
size="sm"
state={{ draft: event }}
colorScheme="primary"
mr="2"
>
Edit
</Button>
<CopyIconButton aria-label="copy json" value={JSON.stringify(event, null, 2)} size="sm" />
</>
}
>
<JsonCode data={event} />
</Section>
<Section label="Threading" p="0">
<JsonCode data={getThreadReferences(event)} />
</Section>
<Section label="Tags">
<ErrorBoundary>
<DebugEventTags event={event} />
</ErrorBoundary>
<Heading size="sm">Tags referenced in content</Heading>
<JsonCode data={getContentTagRefs(event.content, event.tags)} />
</Section>
<Section label="Relays">
<Text>Seen on:</Text>
{Array.from(getSeenRelays(event) ?? []).map((url) => (
<Text gap="1" key={url}>
<RelayFavicon relay={url} size="xs" /> {url}
</Text>
))}
<Button onClick={broadcast} mr="auto" colorScheme="primary" isLoading={loading}>
Broadcast
</Button>
</Section>
{nast && (
<Section label="Parsed Content" p="0">
<JsonCode data={nast.children} />
</Section>
)}
</Accordion>
{tool && (
<Button aria-label="Back" onClick={() => setSelected("")}>
Back
</Button>
)}
</ModalBody>
</ModalContent>
</Modal>

View File

@ -0,0 +1,33 @@
import { CloseButton, Code, Flex, Text } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import useEventUpdate from "../../../hooks/use-event-update";
import { eventStore } from "../../../services/event-store";
export default function DebugEventCachePage({ event }: { event: NostrEvent }) {
useEventUpdate(event.id);
const fields = Object.getOwnPropertySymbols(event);
const update = () => eventStore.update(event);
return (
<Flex direction="column">
{fields.map((field) => (
<Flex gap="2" alignItems="center">
<Text fontWeight="bold" whiteSpace="pre">
{field.description}
</Text>
<Code fontFamily="monospace" isTruncated>
{JSON.stringify(Reflect.get(event, field))}
</Code>
<CloseButton
ml="auto"
onClick={() => {
Reflect.deleteProperty(event, field);
update();
}}
/>
</Flex>
))}
</Flex>
);
}

View File

@ -0,0 +1,24 @@
import { Code, Flex } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { TextNoteContentSymbol } from "applesauce-content/text";
import { Root } from "applesauce-content/nast";
import { CopyButton } from "../../copy-icon-button";
import RawJson from "../raw-json";
export default function DebugContentPage({ event }: { event: NostrEvent }) {
const nast = Reflect.get(event, TextNoteContentSymbol) as Root;
return (
<Flex gap="2" direction="column">
<CopyButton value={event.content} variant="link" size="sm" ml="auto">
Copy content
</CopyButton>
<Code whiteSpace="pre" overflowX="auto" width="100%" p="2">
{event.content}
</Code>
{nast && <RawJson heading="Parsed content" json={nast.children} />}
</Flex>
);
}

View File

@ -0,0 +1,29 @@
import { Button, ButtonGroup, Code } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { Link as RouterLink } from "react-router-dom";
import { EditIcon } from "../../icons";
import { CopyButton } from "../../copy-icon-button";
export default function RawJsonPage({ event }: { event: NostrEvent }) {
return (
<>
<ButtonGroup size="sm" ml="auto">
<CopyButton value={JSON.stringify(event)}>Copy</CopyButton>
<Button
leftIcon={<EditIcon />}
as={RouterLink}
to="/tools/publisher"
state={{ draft: event }}
colorScheme="primary"
>
Edit
</Button>
</ButtonGroup>
<Code whiteSpace="pre" overflowX="auto" width="100%" p="2">
{JSON.stringify(event, null, 2)}
</Code>
</>
);
}

View File

@ -0,0 +1,31 @@
import { useCallback, useState } from "react";
import { NostrEvent } from "nostr-tools";
import { Button, Text } from "@chakra-ui/react";
import { getSeenRelays } from "applesauce-core/helpers";
import { usePublishEvent } from "../../../providers/global/publish-provider";
import { RelayFavicon } from "../../relay-favicon";
export default function DebugEventRelaysPage({ event }: { event: NostrEvent }) {
const publish = usePublishEvent();
const [loading, setLoading] = useState(false);
const broadcast = useCallback(async () => {
setLoading(true);
await publish("Broadcast", event);
setLoading(false);
}, []);
return (
<>
<Text>Seen on:</Text>
{Array.from(getSeenRelays(event) ?? []).map((url) => (
<Text gap="1" key={url}>
<RelayFavicon relay={url} size="xs" /> {url}
</Text>
))}
<Button onClick={broadcast} mr="auto" colorScheme="primary" isLoading={loading}>
Broadcast
</Button>
</>
);
}

View File

@ -0,0 +1,18 @@
import { Flex } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { ErrorBoundary } from "../../error-boundary";
import DebugEventTags from "../event-tags";
import RawJson from "../raw-json";
import { getContentTagRefs } from "../../../helpers/nostr/event";
export default function DebugTagsPage({ event }: { event: NostrEvent }) {
return (
<Flex direction="column" gap="2" alignItems="flex-start" justifyContent="flex-start">
<ErrorBoundary>
<DebugEventTags event={event} />
</ErrorBoundary>
<RawJson heading="Tags referenced in content" json={getContentTagRefs(event.content, event.tags)} />
</Flex>
);
}

View File

@ -0,0 +1,14 @@
import { NostrEvent } from "nostr-tools";
import { getNip10References } from "applesauce-core/helpers";
import { getThreadReferences } from "../../../helpers/nostr/event";
import RawJson from "../raw-json";
export default function DebugThreadingPage({ event }: { event: NostrEvent }) {
return (
<>
<RawJson heading="Legacy" json={getThreadReferences(event)} />
<RawJson heading="NIP-10" json={getNip10References(event)} />
</>
);
}

View File

@ -2,15 +2,13 @@ import { Box, Code, Flex, Heading } from "@chakra-ui/react";
export default function RawJson({ json, heading }: { heading: string; json: any }) {
return (
<Box>
<Box w="full">
<Heading size="sm" mb="2">
{heading}
</Heading>
<Flex gap="2">
<Code whiteSpace="pre" overflowX="auto" width="100%">
{JSON.stringify(json, null, 2)}
</Code>
</Flex>
<Code whiteSpace="pre" overflowX="auto" width="100%" p="2" rounded="md" w="full">
{JSON.stringify(json, null, 2)}
</Code>
</Box>
);
}

View File

@ -1,4 +1,4 @@
import { Box, Code, Flex, Heading } from "@chakra-ui/react";
import { Box, Code, Heading } from "@chakra-ui/react";
import { CopyIconButton } from "../copy-icon-button";
export default function RawValue({ value, heading }: { heading: string; value?: string | null }) {
@ -7,12 +7,10 @@ export default function RawValue({ value, heading }: { heading: string; value?:
<Heading size="sm" mb="2">
{heading}
</Heading>
<Flex gap="2">
<Code fontSize="md" wordBreak="break-all">
{value}
</Code>
<CopyIconButton value={String(value)} size="xs" aria-label="copy" />
</Flex>
<Code py="1" pl="2" fontSize="md" wordBreak="break-all" userSelect="all" fontFamily="monospace" rounded="md">
<CopyIconButton value={String(value)} size="sm" aria-label="copy" variant="ghost" float="right" ml="2" />
{value}
</Code>
</Box>
);
}

View File

@ -1,3 +1,4 @@
import { useMemo } from "react";
import { Box, Button, ButtonProps, Icon, Link, Text, others, useDisclosure } from "@chakra-ui/react";
import { Link as RouterLink, useLocation } from "react-router-dom";
import { nip19 } from "nostr-tools";
@ -26,7 +27,7 @@ import KeyboardShortcut from "../keyboard-shortcut";
import useRecentIds from "../../hooks/use-recent-ids";
import { internalApps, internalTools } from "../../views/other-stuff/apps";
import { App, AppIcon } from "../../views/other-stuff/component/app-card";
import { useMemo } from "react";
import Wallet02 from "../icons/wallet-02";
export default function NavItems() {
const location = useLocation();
@ -49,6 +50,7 @@ export default function NavItems() {
else if (location.pathname.startsWith("/notifications")) active = "notifications";
else if (location.pathname.startsWith("/launchpad")) active = "launchpad";
else if (location.pathname.startsWith("/discovery")) active = "discovery";
else if (location.pathname.startsWith("/wallet")) active = "wallet";
else if (location.pathname.startsWith("/dm")) active = "dm";
else if (location.pathname.startsWith("/streams")) active = "streams";
else if (location.pathname.startsWith("/relays")) active = "relays";
@ -149,6 +151,15 @@ export default function NavItems() {
Messages
{showShortcuts && <KeyboardShortcut letter="m" requireMeta ml="auto" />}
</Button>
{/* <Button
as={RouterLink}
to="/wallet"
leftIcon={<Wallet02 boxSize={6} />}
colorScheme={active === "wallet" ? "primary" : undefined}
{...buttonProps}
>
Wallet
</Button> */}
</>
)}
<Button

View File

@ -0,0 +1,10 @@
import { getTagValue } from "applesauce-core/helpers";
import { NostrEvent } from "nostr-tools";
export function getWalletName(wallet: NostrEvent) {
return getTagValue(wallet, "name");
}
export function getWalletDescription(wallet: NostrEvent) {
return getTagValue(wallet, "description");
}

View File

@ -0,0 +1,13 @@
import { useEffect, useMemo, useState } from "react";
import { eventStore } from "../services/event-store";
export default function useEventUpdate(id?: string) {
const [_count, setCount] = useState(0);
const observable = useMemo(() => (id ? eventStore.event(id) : undefined), [id]);
useEffect(() => {
if (!observable) return;
const sub = observable.subscribe(() => setCount((v) => v + 1));
return () => sub.unsubscribe();
}, [observable]);
}

View File

@ -0,0 +1,68 @@
import { Badge, Button, Card, CardBody, CardFooter, CardHeader, Flex, Heading } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import {
getEventUID,
getTagValue,
hasHiddenTags,
HiddenTagsSigner,
isHiddenTagsLocked,
unlockHiddenTags,
} from "applesauce-core/helpers";
import { useReadRelays } from "../../hooks/use-client-relays";
import useCurrentAccount from "../../hooks/use-current-account";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useAsyncErrorHandler from "../../hooks/use-async-error-handler";
import { getWalletDescription, getWalletName } from "../../helpers/nostr/wallet";
import DebugEventButton from "../../components/debug-modal/debug-event-button";
import useEventUpdate from "../../hooks/use-event-update";
import { eventStore } from "../../services/event-store";
function Wallet({ wallet }: { wallet: NostrEvent }) {
useEventUpdate(wallet.id);
const account = useCurrentAccount()!;
const locked = hasHiddenTags(wallet) && isHiddenTagsLocked(wallet);
const unlock = useAsyncErrorHandler(async () => {
const signer = account.signer;
if (!signer || !signer.nip04) throw new Error("Missing signer");
await unlockHiddenTags(wallet, signer as HiddenTagsSigner, eventStore);
}, [wallet, account]);
return (
<Card>
<CardHeader display="flex" gap="2" p="2" alignItems="center">
<Heading size="md">{getWalletName(wallet) || getTagValue(wallet, "d")}</Heading>
{locked && <Badge colorScheme="orange">Locked</Badge>}
<DebugEventButton event={wallet} variant="ghost" ml="auto" size="sm" />
</CardHeader>
<CardBody px="2" py="0">
{getWalletDescription(wallet)}
</CardBody>
<CardFooter p="2">
{locked && (
<Button onClick={unlock} colorScheme="primary">
Unlock
</Button>
)}
</CardFooter>
</Card>
);
}
export default function WalletView() {
const account = useCurrentAccount()!;
const readRelays = useReadRelays();
const { timeline } = useTimelineLoader("wallets", readRelays, { kinds: [37375], authors: [account.pubkey] });
return (
<Flex direction="column" gap="2">
{timeline.map((wallet) => (
<Wallet key={getEventUID(wallet)} wallet={wallet} />
))}
</Flex>
);
}