mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-17 19:14:04 +02:00
add event publisher tool
This commit is contained in:
5
.changeset/fresh-books-dream.md
Normal file
5
.changeset/fresh-books-dream.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add event publisher tool
|
@@ -101,6 +101,7 @@ const NetworkMuteGraphView = lazy(() => import("./views/tools/network-mute-graph
|
|||||||
const NetworkDMGraphView = lazy(() => import("./views/tools/network-dm-graph"));
|
const NetworkDMGraphView = lazy(() => import("./views/tools/network-dm-graph"));
|
||||||
const UnknownTimelineView = lazy(() => import("./views/tools/unknown-event-feed"));
|
const UnknownTimelineView = lazy(() => import("./views/tools/unknown-event-feed"));
|
||||||
const EventConsoleView = lazy(() => import("./views/tools/event-console"));
|
const EventConsoleView = lazy(() => import("./views/tools/event-console"));
|
||||||
|
const EventPublisherView = lazy(() => import("./views/tools/event-publisher"));
|
||||||
|
|
||||||
const UserStreamsTab = lazy(() => import("./views/user/streams"));
|
const UserStreamsTab = lazy(() => import("./views/user/streams"));
|
||||||
const StreamsView = lazy(() => import("./views/streams"));
|
const StreamsView = lazy(() => import("./views/streams"));
|
||||||
@@ -323,6 +324,7 @@ const router = createHashRouter([
|
|||||||
{ path: "satellite-cdn", element: <SatelliteCDNView /> },
|
{ path: "satellite-cdn", element: <SatelliteCDNView /> },
|
||||||
{ path: "unknown", element: <UnknownTimelineView /> },
|
{ path: "unknown", element: <UnknownTimelineView /> },
|
||||||
{ path: "console", element: <EventConsoleView /> },
|
{ path: "console", element: <EventConsoleView /> },
|
||||||
|
{ path: "publisher", element: <EventPublisherView /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -19,6 +19,7 @@ import ShieldOff from "../../components/icons/shield-off";
|
|||||||
import Users01 from "../../components/icons/users-01";
|
import Users01 from "../../components/icons/users-01";
|
||||||
import Film02 from "../../components/icons/film-02";
|
import Film02 from "../../components/icons/film-02";
|
||||||
import MessageQuestionSquare from "../../components/icons/message-question-square";
|
import MessageQuestionSquare from "../../components/icons/message-question-square";
|
||||||
|
import UploadCloud01 from "../../components/icons/upload-cloud-01";
|
||||||
|
|
||||||
export const internalApps: App[] = [
|
export const internalApps: App[] = [
|
||||||
{
|
{
|
||||||
@@ -96,6 +97,13 @@ export const internalTools: App[] = [
|
|||||||
id: "console",
|
id: "console",
|
||||||
to: "/tools/console",
|
to: "/tools/console",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Event Publisher",
|
||||||
|
description: "Write and publish events",
|
||||||
|
icon: UploadCloud01,
|
||||||
|
id: "publisher",
|
||||||
|
to: "/tools/publisher ",
|
||||||
|
},
|
||||||
{ title: "WoT Test", description: "Just a test for now", icon: Users01, id: "wot-test", to: "/tools/wot-test" },
|
{ title: "WoT Test", description: "Just a test for now", icon: Users01, id: "wot-test", to: "/tools/wot-test" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
import { Box, Code, Flex, Heading, Text, useDisclosure } from "@chakra-ui/react";
|
import { Box, Code, Flex, Heading, Switch, Text, useDisclosure } from "@chakra-ui/react";
|
||||||
import { NostrEvent } from "nostr-tools";
|
import { NostrEvent } from "nostr-tools";
|
||||||
import ExpandButton from "./expand-button";
|
import ExpandButton from "./expand-button";
|
||||||
import UserName from "../../../components/user/user-name";
|
import UserName from "../../../components/user/user-name";
|
||||||
import { CopyIconButton } from "../../../components/copy-icon-button";
|
import { CopyIconButton } from "../../../components/copy-icon-button";
|
||||||
import Timestamp from "../../../components/timestamp";
|
import Timestamp from "../../../components/timestamp";
|
||||||
|
import stringify from "json-stringify-deterministic";
|
||||||
|
|
||||||
export default function EventRow({ event }: { event: NostrEvent }) {
|
export default function EventRow({ event }: { event: NostrEvent }) {
|
||||||
const expanded = useDisclosure();
|
const expanded = useDisclosure();
|
||||||
|
const raw = useDisclosure();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -32,7 +34,28 @@ export default function EventRow({ event }: { event: NostrEvent }) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{expanded.isOpen && (
|
{expanded.isOpen && (
|
||||||
<Flex gap="2" direction="column" px="2" pb="2" alignItems="flex-start">
|
<Flex gap="2" direction="column" px="2" pb="2" alignItems="flex-start" position="relative">
|
||||||
|
<Flex
|
||||||
|
top="2"
|
||||||
|
right="4"
|
||||||
|
position="absolute"
|
||||||
|
gap="2"
|
||||||
|
alignItems="center"
|
||||||
|
p="2"
|
||||||
|
bg="var(--chakra-colors-chakra-body-bg)"
|
||||||
|
borderRadius="md"
|
||||||
|
>
|
||||||
|
{raw.isOpen && <CopyIconButton text={stringify(event, { space: " " })} aria-label="Copy json" size="sm" />}
|
||||||
|
<Switch size="sm" checked={!raw.isOpen} onChange={raw.onToggle}>
|
||||||
|
Raw
|
||||||
|
</Switch>
|
||||||
|
</Flex>
|
||||||
|
{raw.isOpen ? (
|
||||||
|
<Code whiteSpace="pre" w="full" overflow="auto">
|
||||||
|
{stringify(event, { space: " " })}
|
||||||
|
</Code>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<Text>ID: </Text>
|
<Text>ID: </Text>
|
||||||
<Code>{event.id}</Code>
|
<Code>{event.id}</Code>
|
||||||
@@ -65,6 +88,8 @@ export default function EventRow({ event }: { event: NostrEvent }) {
|
|||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
64
src/views/tools/event-console/filter-editor.tsx
Normal file
64
src/views/tools/event-console/filter-editor.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { memo, useMemo } from "react";
|
||||||
|
import { useColorMode } from "@chakra-ui/react";
|
||||||
|
import ReactCodeMirror from "@uiw/react-codemirror";
|
||||||
|
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||||
|
import { jsonSchema } from "codemirror-json-schema";
|
||||||
|
import { keymap } from "@codemirror/view";
|
||||||
|
import { useInterval } from "react-use";
|
||||||
|
import _throttle from "lodash.throttle";
|
||||||
|
import { CompletionContext, CompletionResult } from "@codemirror/autocomplete";
|
||||||
|
import { jsonLanguage } from "@codemirror/lang-json";
|
||||||
|
import { syntaxTree } from "@codemirror/language";
|
||||||
|
|
||||||
|
import { NostrFilterSchema } from "./schema";
|
||||||
|
import { UserDirectory, useUserSearchDirectoryContext } from "../../../providers/global/user-directory-provider";
|
||||||
|
import { codeMirrorUserAutocomplete, updateCodeMirrorUserAutocomplete } from "./user-autocomplete";
|
||||||
|
|
||||||
|
const FilterEditor = memo(
|
||||||
|
({ value, onChange, onRun }: { value: string; onChange: (v: string) => void; onRun: () => void }) => {
|
||||||
|
const getDirectory = useUserSearchDirectoryContext();
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
updateCodeMirrorUserAutocomplete(getDirectory());
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const extensions = useMemo(
|
||||||
|
() => [
|
||||||
|
keymap.of([
|
||||||
|
{
|
||||||
|
win: "Ctrl-Enter",
|
||||||
|
linux: "Ctrl-Enter",
|
||||||
|
mac: "Cmd-Enter",
|
||||||
|
preventDefault: true,
|
||||||
|
run: () => {
|
||||||
|
onRun();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
shift: () => {
|
||||||
|
onRun();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
jsonSchema(NostrFilterSchema),
|
||||||
|
jsonLanguage.data.of({
|
||||||
|
autocomplete: codeMirrorUserAutocomplete,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[onRun],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<ReactCodeMirror
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
height="200px"
|
||||||
|
lang="json"
|
||||||
|
extensions={extensions}
|
||||||
|
theme={colorMode === "light" ? githubLight : githubDark}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default FilterEditor;
|
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useMemo, useRef, useState } from "react";
|
import { memo, useCallback, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
AlertDescription,
|
AlertDescription,
|
||||||
@@ -13,24 +13,15 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
useColorMode,
|
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import ReactCodeMirror from "@uiw/react-codemirror";
|
|
||||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
|
||||||
import { jsonSchema } from "codemirror-json-schema";
|
|
||||||
import { NostrEvent, Relay, Subscription } from "nostr-tools";
|
import { NostrEvent, Relay, Subscription } from "nostr-tools";
|
||||||
import { keymap } from "@codemirror/view";
|
import { useLocalStorage } from "react-use";
|
||||||
import { useInterval, useLocalStorage } from "react-use";
|
|
||||||
import { Subscription as IDBSubscription, CacheRelay } from "nostr-idb";
|
import { Subscription as IDBSubscription, CacheRelay } from "nostr-idb";
|
||||||
import _throttle from "lodash.throttle";
|
import _throttle from "lodash.throttle";
|
||||||
import { CompletionContext, CompletionResult } from "@codemirror/autocomplete";
|
|
||||||
import { jsonLanguage } from "@codemirror/lang-json";
|
|
||||||
import { syntaxTree } from "@codemirror/language";
|
|
||||||
|
|
||||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||||
import BackButton from "../../../components/router/back-button";
|
import BackButton from "../../../components/router/back-button";
|
||||||
import { NostrFilterSchema } from "./schema";
|
|
||||||
import { localRelay } from "../../../services/local-relay";
|
import { localRelay } from "../../../services/local-relay";
|
||||||
import Play from "../../../components/icons/play";
|
import Play from "../../../components/icons/play";
|
||||||
import ClockRewind from "../../../components/icons/clock-rewind";
|
import ClockRewind from "../../../components/icons/clock-rewind";
|
||||||
@@ -43,78 +34,7 @@ import stringify from "json-stringify-deterministic";
|
|||||||
import { DownloadIcon } from "../../../components/icons";
|
import { DownloadIcon } from "../../../components/icons";
|
||||||
import { RelayUrlInput } from "../../../components/relay-url-input";
|
import { RelayUrlInput } from "../../../components/relay-url-input";
|
||||||
import { validateRelayURL } from "../../../helpers/relay";
|
import { validateRelayURL } from "../../../helpers/relay";
|
||||||
import { UserDirectory, useUserSearchDirectoryContext } from "../../../providers/global/user-directory-provider";
|
import FilterEditor from "./filter-editor";
|
||||||
|
|
||||||
let users: UserDirectory = [];
|
|
||||||
function userAutocomplete(context: CompletionContext): CompletionResult | null {
|
|
||||||
let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
|
|
||||||
if (nodeBefore.name !== "String") return null;
|
|
||||||
|
|
||||||
let textBefore = context.state.sliceDoc(nodeBefore.from, context.pos);
|
|
||||||
let tagBefore = /@\w*$/.exec(textBefore);
|
|
||||||
if (!tagBefore && !context.explicit) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos,
|
|
||||||
validFor: /^(@\w*)?$/,
|
|
||||||
// options: tagOptions,
|
|
||||||
options: users
|
|
||||||
.filter((u) => !!u.names[0])
|
|
||||||
.map((user) => ({
|
|
||||||
label: "@" + user.names[0]!,
|
|
||||||
type: "keyword",
|
|
||||||
apply: user.pubkey,
|
|
||||||
detail: "pubkey",
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const FilterEditor = memo(
|
|
||||||
({ value, onChange, onRun }: { value: string; onChange: (v: string) => void; onRun: () => void }) => {
|
|
||||||
const getDirectory = useUserSearchDirectoryContext();
|
|
||||||
const { colorMode } = useColorMode();
|
|
||||||
|
|
||||||
useInterval(() => {
|
|
||||||
users = getDirectory();
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
const extensions = useMemo(
|
|
||||||
() => [
|
|
||||||
keymap.of([
|
|
||||||
{
|
|
||||||
win: "Ctrl-Enter",
|
|
||||||
linux: "Ctrl-Enter",
|
|
||||||
mac: "Cmd-Enter",
|
|
||||||
preventDefault: true,
|
|
||||||
run: () => {
|
|
||||||
onRun();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
shift: () => {
|
|
||||||
onRun();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
jsonSchema(NostrFilterSchema),
|
|
||||||
jsonLanguage.data.of({
|
|
||||||
autocomplete: userAutocomplete,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
[onRun],
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<ReactCodeMirror
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
height="200px"
|
|
||||||
lang="json"
|
|
||||||
extensions={extensions}
|
|
||||||
theme={colorMode === "light" ? githubLight : githubDark}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const EventTimeline = memo(({ events }: { events: NostrEvent[] }) => {
|
const EventTimeline = memo(({ events }: { events: NostrEvent[] }) => {
|
||||||
return (
|
return (
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Filter, nip19 } from "nostr-tools";
|
import { Filter, nip19 } from "nostr-tools";
|
||||||
|
|
||||||
function processDateString(date: string) {
|
export function processDateString(date: string) {
|
||||||
if (date.startsWith("n")) {
|
if (date.toLowerCase() === "now" || date.toLowerCase() === "n") {
|
||||||
|
return dayjs().unix();
|
||||||
|
} else if (date.startsWith("n")) {
|
||||||
const match = date.match(/n([+-])(\d+)([hwmsd])?/i);
|
const match = date.match(/n([+-])(\d+)([hwmsd])?/i);
|
||||||
if (match === null) throw new Error(`Cant parse relative date string ${date}`);
|
if (match === null) throw new Error(`Cant parse relative date string ${date}`);
|
||||||
|
|
||||||
@@ -21,8 +23,6 @@ function processDateString(date: string) {
|
|||||||
.unix()
|
.unix()
|
||||||
);
|
);
|
||||||
} else throw Error(`Unknown operation ${match[1]}`);
|
} else throw Error(`Unknown operation ${match[1]}`);
|
||||||
} else if (date.toLowerCase() === "now") {
|
|
||||||
return dayjs().unix();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unknown date string ${date}`);
|
throw new Error(`Unknown date string ${date}`);
|
||||||
|
33
src/views/tools/event-console/user-autocomplete.ts
Normal file
33
src/views/tools/event-console/user-autocomplete.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import _throttle from "lodash.throttle";
|
||||||
|
import { CompletionContext, CompletionResult } from "@codemirror/autocomplete";
|
||||||
|
import { syntaxTree } from "@codemirror/language";
|
||||||
|
|
||||||
|
import { UserDirectory } from "../../../providers/global/user-directory-provider";
|
||||||
|
|
||||||
|
let users: UserDirectory = [];
|
||||||
|
export function codeMirrorUserAutocomplete(context: CompletionContext): CompletionResult | null {
|
||||||
|
let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
|
||||||
|
if (nodeBefore.name !== "String") return null;
|
||||||
|
|
||||||
|
let textBefore = context.state.sliceDoc(nodeBefore.from, context.pos);
|
||||||
|
let tagBefore = /@\w*$/.exec(textBefore);
|
||||||
|
if (!tagBefore && !context.explicit) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos,
|
||||||
|
validFor: /^(@\w*)?$/,
|
||||||
|
// options: tagOptions,
|
||||||
|
options: users
|
||||||
|
.filter((u) => !!u.names[0])
|
||||||
|
.map((user) => ({
|
||||||
|
label: "@" + user.names[0]!,
|
||||||
|
type: "keyword",
|
||||||
|
apply: user.pubkey,
|
||||||
|
detail: "pubkey",
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCodeMirrorUserAutocomplete(newUsers: UserDirectory) {
|
||||||
|
users = newUsers;
|
||||||
|
}
|
62
src/views/tools/event-publisher/event-editor.tsx
Normal file
62
src/views/tools/event-publisher/event-editor.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { memo, useMemo } from "react";
|
||||||
|
import { useColorMode } from "@chakra-ui/react";
|
||||||
|
import ReactCodeMirror from "@uiw/react-codemirror";
|
||||||
|
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||||
|
import { jsonSchema } from "codemirror-json-schema";
|
||||||
|
import { keymap } from "@codemirror/view";
|
||||||
|
import { useInterval } from "react-use";
|
||||||
|
import _throttle from "lodash.throttle";
|
||||||
|
import { jsonLanguage } from "@codemirror/lang-json";
|
||||||
|
|
||||||
|
import { NostrEventSchema } from "./schema";
|
||||||
|
import { useUserSearchDirectoryContext } from "../../../providers/global/user-directory-provider";
|
||||||
|
import { updateCodeMirrorUserAutocomplete, codeMirrorUserAutocomplete } from "../event-console/user-autocomplete";
|
||||||
|
|
||||||
|
const EventEditor = memo(
|
||||||
|
({ value, onChange, onRun }: { value: string; onChange: (v: string) => void; onRun?: () => void }) => {
|
||||||
|
const getDirectory = useUserSearchDirectoryContext();
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
updateCodeMirrorUserAutocomplete(getDirectory());
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const extensions = useMemo(
|
||||||
|
() => [
|
||||||
|
keymap.of([
|
||||||
|
{
|
||||||
|
win: "Ctrl-Enter",
|
||||||
|
linux: "Ctrl-Enter",
|
||||||
|
mac: "Cmd-Enter",
|
||||||
|
preventDefault: true,
|
||||||
|
run: () => {
|
||||||
|
if (onRun) onRun();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
shift: () => {
|
||||||
|
if (onRun) onRun();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
jsonSchema(NostrEventSchema),
|
||||||
|
jsonLanguage.data.of({
|
||||||
|
autocomplete: codeMirrorUserAutocomplete,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[onRun],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<ReactCodeMirror
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
height="200px"
|
||||||
|
lang="json"
|
||||||
|
extensions={extensions}
|
||||||
|
theme={colorMode === "light" ? githubLight : githubDark}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EventEditor;
|
170
src/views/tools/event-publisher/index.tsx
Normal file
170
src/views/tools/event-publisher/index.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Code,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
useToast,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||||
|
import BackButton from "../../../components/router/back-button";
|
||||||
|
import Play from "../../../components/icons/play";
|
||||||
|
import EventEditor from "./event-editor";
|
||||||
|
import { EventTemplate, NostrEvent, UnsignedEvent, getEventHash, verifyEvent } from "nostr-tools";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { processEvent } from "./process";
|
||||||
|
import { WritingIcon } from "../../../components/icons";
|
||||||
|
import { useSigningContext } from "../../../providers/global/signing-provider";
|
||||||
|
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||||
|
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||||
|
import UserAvatar from "../../../components/user/user-avatar";
|
||||||
|
|
||||||
|
export default function EventPublisherView() {
|
||||||
|
const toast = useToast();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { requestSignature } = useSigningContext();
|
||||||
|
const publish = usePublishEvent();
|
||||||
|
const account = useCurrentAccount();
|
||||||
|
|
||||||
|
const defaultEvent = useMemo(
|
||||||
|
() =>
|
||||||
|
JSON.stringify(
|
||||||
|
{ kind: 1234, content: "", tags: [], created_at: dayjs().unix() } satisfies EventTemplate,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [value, setValue] = useState(defaultEvent);
|
||||||
|
const [draft, setDraft] = useState<NostrEvent | EventTemplate>();
|
||||||
|
|
||||||
|
const submitEvent = () => {
|
||||||
|
try {
|
||||||
|
const draft = processEvent(JSON.parse(value) as UnsignedEvent);
|
||||||
|
if (account) draft.pubkey = account.pubkey;
|
||||||
|
(draft as NostrEvent).id = getEventHash(draft);
|
||||||
|
setDraft(draft);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sign = async () => {
|
||||||
|
if (!draft) return;
|
||||||
|
try {
|
||||||
|
setDraft(await requestSignature(draft));
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishDraft = async () => {
|
||||||
|
if (!draft || !(draft as NostrEvent).sig) return;
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const valid = verifyEvent(draft as NostrEvent);
|
||||||
|
if (!valid) throw new Error("Invalid event");
|
||||||
|
await publish("Custom Event", draft);
|
||||||
|
setDraft(undefined);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const yolo = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const draft = processEvent(JSON.parse(value) as UnsignedEvent);
|
||||||
|
if (account) draft.pubkey = account.pubkey;
|
||||||
|
(draft as NostrEvent).id = getEventHash(draft);
|
||||||
|
const event = await requestSignature(draft);
|
||||||
|
const valid = verifyEvent(event);
|
||||||
|
if (!valid) throw new Error("Invalid event");
|
||||||
|
await publish("Custom Event", event);
|
||||||
|
setDraft(undefined);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<VerticalPageLayout>
|
||||||
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
|
<BackButton size="sm" />
|
||||||
|
<Heading size="md">Event Publisher</Heading>
|
||||||
|
<ButtonGroup ml="auto">
|
||||||
|
{/* <IconButton icon={<HelpCircle />} aria-label="Help" title="Help" size="sm" onClick={helpModal.onOpen} /> */}
|
||||||
|
<Button colorScheme="primary" onClick={submitEvent} leftIcon={<Play />} size="sm">
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<EventEditor value={value} onChange={(v) => setValue(v)} onRun={submitEvent} />
|
||||||
|
</VerticalPageLayout>
|
||||||
|
{draft && (
|
||||||
|
<Modal isOpen onClose={() => setDraft(undefined)} size="2xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader p="4">Publish Event</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody px="4" pb="2" pt="0">
|
||||||
|
<Heading size="md" mt="2">
|
||||||
|
1. Event ID
|
||||||
|
</Heading>
|
||||||
|
<Code w="full" p="2" overflow="auto">
|
||||||
|
{(draft as NostrEvent).id}
|
||||||
|
</Code>
|
||||||
|
<Heading size="md" mt="2">
|
||||||
|
2. Pubkey
|
||||||
|
</Heading>
|
||||||
|
<Flex gap="2" alignItems="center">
|
||||||
|
<Code w="full" p="2" overflow="auto">
|
||||||
|
{(draft as NostrEvent).pubkey}
|
||||||
|
</Code>
|
||||||
|
<UserAvatar pubkey={(draft as NostrEvent).pubkey} />
|
||||||
|
</Flex>
|
||||||
|
<Heading size="md" whiteSpace="pre" mt="2">
|
||||||
|
3. Signature
|
||||||
|
</Heading>
|
||||||
|
<Flex gap="2" alignItems="center">
|
||||||
|
<Code overflow="auto" whiteSpace="pre" w="full" p="2">
|
||||||
|
{(draft as NostrEvent).sig}
|
||||||
|
</Code>
|
||||||
|
<Button leftIcon={<WritingIcon boxSize={5} />} flexShrink={0} onClick={sign} ml="auto">
|
||||||
|
Sign
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{(draft as NostrEvent).sig && (
|
||||||
|
<Button w="full" colorScheme="primary" mt="2" isLoading={loading} onClick={publishDraft}>
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button mr={2} onClick={() => setDraft(undefined)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme="primary" onClick={yolo} isLoading={loading}>
|
||||||
|
Yolo
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
10
src/views/tools/event-publisher/process.ts
Normal file
10
src/views/tools/event-publisher/process.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { EventTemplate, UnsignedEvent } from "nostr-tools";
|
||||||
|
import { processDateString } from "../event-console/process";
|
||||||
|
|
||||||
|
export function processEvent(event: UnsignedEvent): UnsignedEvent {
|
||||||
|
if (typeof event.created_at === "string") {
|
||||||
|
event.created_at = processDateString(event.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
44
src/views/tools/event-publisher/schema.ts
Normal file
44
src/views/tools/event-publisher/schema.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import { type JSONSchema7 } from "json-schema";
|
||||||
|
|
||||||
|
export const NostrEventSchema: JSONSchema7 = {
|
||||||
|
type: "object",
|
||||||
|
required: ["kind", "created_at", "tags", "content"],
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
description: "The id of the event",
|
||||||
|
},
|
||||||
|
kind: {
|
||||||
|
type: "integer",
|
||||||
|
description: "The kind of event",
|
||||||
|
minimum: 0,
|
||||||
|
},
|
||||||
|
pubkey: {
|
||||||
|
type: "string",
|
||||||
|
description: "The owner of the event",
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
description: "The unix timestamp the event was created at",
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: "integer",
|
||||||
|
minimum: 0,
|
||||||
|
default: dayjs().unix(),
|
||||||
|
},
|
||||||
|
{ type: "string" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: "array",
|
||||||
|
description: "Event metadata tags",
|
||||||
|
items: {
|
||||||
|
type: "array",
|
||||||
|
minItems: 1,
|
||||||
|
items: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
Reference in New Issue
Block a user