mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-29 04:52:59 +02:00
Add templates to event publisher
added edit button to event debug modal
This commit is contained in:
5
.changeset/blue-moose-laugh.md
Normal file
5
.changeset/blue-moose-laugh.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add edit button to event debug modal
|
5
.changeset/cyan-knives-return.md
Normal file
5
.changeset/cyan-knives-return.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add templates to event publisher
|
@@ -55,6 +55,7 @@
|
||||
"emoji-regex": "^10.3.0",
|
||||
"emojilib": "^3",
|
||||
"framer-motion": "^10.16.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"hls.js": "^1.4.14",
|
||||
"i18n-iso-countries": "^7.12.0",
|
||||
"idb": "^8.0.0",
|
||||
|
@@ -237,6 +237,14 @@ const router = createHashRouter([
|
||||
</RouteProviders>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/tools/publisher",
|
||||
element: (
|
||||
<RouteProviders>
|
||||
<EventPublisherView />
|
||||
</RouteProviders>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
element: <RootPage />,
|
||||
@@ -392,7 +400,6 @@ const router = createHashRouter([
|
||||
{ path: "satellite-cdn", element: <SatelliteCDNView /> },
|
||||
{ path: "unknown", element: <UnknownTimelineView /> },
|
||||
{ path: "console", element: <EventConsoleView /> },
|
||||
{ path: "publisher", element: <EventPublisherView /> },
|
||||
{ path: "corrections", element: <CorrectionsFeedView /> },
|
||||
],
|
||||
},
|
||||
|
@@ -17,6 +17,7 @@ import {
|
||||
AccordionPanelProps,
|
||||
Button,
|
||||
} from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { ModalProps } from "@chakra-ui/react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
@@ -27,6 +28,7 @@ import { CopyIconButton } from "../copy-icon-button";
|
||||
import DebugEventTags from "./event-tags";
|
||||
import relayHintService from "../../services/event-relay-hint";
|
||||
import { usePublishEvent } from "../../providers/global/publish-provider";
|
||||
import { EditIcon } from "../icons";
|
||||
|
||||
function Section({
|
||||
label,
|
||||
@@ -112,7 +114,22 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent
|
||||
<Section
|
||||
label="JSON"
|
||||
p="0"
|
||||
actions={<CopyIconButton aria-label="copy json" value={JSON.stringify(event, null, 2)} size="sm" />}
|
||||
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>
|
||||
|
@@ -1,20 +1,28 @@
|
||||
import { Input, InputProps } from "@chakra-ui/react";
|
||||
import { forwardRef } from "react";
|
||||
import { useAsync } from "react-use";
|
||||
import { forwardRef, useEffect, useState } from "react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { useUserSearchDirectoryContext } from "../providers/global/user-directory-provider";
|
||||
import userMetadataService from "../services/user-metadata";
|
||||
import { getDisplayName } from "../helpers/nostr/user-metadata";
|
||||
import { getDisplayName, Kind0ParsedContent } from "../helpers/nostr/user-metadata";
|
||||
import useAppSettings from "../hooks/use-app-settings";
|
||||
|
||||
const UserAutocomplete = forwardRef<HTMLInputElement, InputProps>(({ value, ...props }, ref) => {
|
||||
type UserAutocompleteProps = InputProps & {
|
||||
hex?: boolean;
|
||||
};
|
||||
|
||||
const UserAutocomplete = forwardRef<HTMLInputElement, UserAutocompleteProps>(({ value, hex, ...props }, ref) => {
|
||||
const getDirectory = useUserSearchDirectoryContext();
|
||||
const { removeEmojisInUsernames } = useAppSettings();
|
||||
|
||||
const { value: users } = useAsync(async () => {
|
||||
const dir = await getDirectory();
|
||||
return dir.map(({ pubkey }) => ({ pubkey, metadata: userMetadataService.getSubject(pubkey).value }));
|
||||
const [users, setUsers] = useState<{ pubkey: string; names: string[]; metadata?: Kind0ParsedContent }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const dir = getDirectory();
|
||||
|
||||
setUsers(
|
||||
dir.map(({ pubkey, names }) => ({ pubkey, names, metadata: userMetadataService.getSubject(pubkey).value })),
|
||||
);
|
||||
}, [getDirectory]);
|
||||
|
||||
return (
|
||||
@@ -22,13 +30,11 @@ const UserAutocomplete = forwardRef<HTMLInputElement, InputProps>(({ value, ...p
|
||||
<Input placeholder="Users" list="users" value={value} {...props} ref={ref} />
|
||||
{users && (
|
||||
<datalist id="users">
|
||||
{users
|
||||
.filter((p) => !!p.metadata)
|
||||
.map(({ metadata, pubkey }) => (
|
||||
<option key={pubkey} value={nip19.npubEncode(pubkey)}>
|
||||
{getDisplayName(metadata, pubkey, removeEmojisInUsernames)}
|
||||
</option>
|
||||
))}
|
||||
{users.map(({ metadata, pubkey, names }) => (
|
||||
<option key={pubkey} value={hex ? pubkey : nip19.npubEncode(pubkey)}>
|
||||
{names[0] || getDisplayName(metadata, pubkey, removeEmojisInUsernames)}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
)}
|
||||
</>
|
||||
|
@@ -0,0 +1,39 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { Flex, IconButton, Input, InputProps } from "@chakra-ui/react";
|
||||
|
||||
import ClockStopwatch from "../../../../components/icons/clock-stopwatch";
|
||||
|
||||
export default function TimestampInput({
|
||||
timestamp,
|
||||
onChange,
|
||||
...props
|
||||
}: { timestamp?: number; onChange: (ts: number) => void } & Omit<InputProps, "type" | "value" | "onChange">) {
|
||||
const [value, setValue] = useState(() => (timestamp ? dayjs.unix(timestamp) : dayjs()).format("YYYY-MM-DDTHH:mm"));
|
||||
|
||||
useEffect(() => {
|
||||
if (timestamp) {
|
||||
setValue(dayjs.unix(timestamp).format("YYYY-MM-DDTHH:mm"));
|
||||
}
|
||||
}, [timestamp]);
|
||||
|
||||
const handleDateChange = (value: string) => {
|
||||
setValue(value);
|
||||
const date = dayjs(value, "YYYY-MM-DDTHH:mm");
|
||||
if (date.isValid()) {
|
||||
onChange(date.unix());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex gap="2" flex={1}>
|
||||
<Input type="datetime-local" value={value} onChange={(e) => handleDateChange(e.target.value)} {...props} />
|
||||
<IconButton
|
||||
icon={<ClockStopwatch boxSize={5} />}
|
||||
aria-label="Set to now"
|
||||
onClick={() => onChange(dayjs().unix())}
|
||||
size={props.size}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
ButtonGroup,
|
||||
CloseButton,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Heading,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
} from "@chakra-ui/react";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { LooseEventTemplate } from "../../process";
|
||||
|
||||
import MagicTextArea from "../../../../../components/magic-textarea";
|
||||
import TimestampInput from "../datetime-input";
|
||||
import Plus from "../../../../../components/icons/plus";
|
||||
import Minus from "../../../../../components/icons/minus";
|
||||
|
||||
const KindOptions = (
|
||||
Object.entries(kinds).filter(([name, value]) => typeof value === "number") as [string, number][]
|
||||
).sort((a, b) => a[1] - b[1]);
|
||||
|
||||
export default function EventTemplateEditor({
|
||||
draft,
|
||||
onChange,
|
||||
}: {
|
||||
draft: LooseEventTemplate;
|
||||
onChange: (draft: LooseEventTemplate) => void;
|
||||
}) {
|
||||
const setTagValue = (index: number, tagIndex: number, value: string) => {
|
||||
onChange({
|
||||
...draft,
|
||||
tags: draft.tags.map((t, i) => {
|
||||
if (i === index) {
|
||||
const newTag = Array.from(t);
|
||||
newTag[tagIndex] = value;
|
||||
return newTag;
|
||||
}
|
||||
return t;
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const removeTag = (index: number) => {
|
||||
const tags = Array.from(draft.tags);
|
||||
tags.splice(index, 1);
|
||||
|
||||
onChange({ ...draft, tags });
|
||||
};
|
||||
|
||||
const addTagValue = (index: number) => {
|
||||
onChange({
|
||||
...draft,
|
||||
tags: draft.tags.map((tag, i) => {
|
||||
if (i === index) return [...tag, ""];
|
||||
return tag;
|
||||
}),
|
||||
});
|
||||
};
|
||||
const removeTagValue = (index: number) => {
|
||||
onChange({
|
||||
...draft,
|
||||
tags: draft.tags.map((tag, i) => {
|
||||
if (i === index) return tag.slice(0, -1);
|
||||
return tag;
|
||||
}),
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="content">Kind</FormLabel>
|
||||
<Select value={draft.kind} onChange={(e) => onChange({ ...draft, kind: parseInt(e.target.value) })}>
|
||||
{KindOptions.map(([name, kind]) => (
|
||||
<option value={kind} key={name + kind}>
|
||||
{name} ({kind})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="content">Content</FormLabel>
|
||||
<MagicTextArea
|
||||
id="content"
|
||||
name="content"
|
||||
value={draft.content}
|
||||
onChange={(e) => onChange({ ...draft, content: e.target.value })}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="content">Created At</FormLabel>
|
||||
<TimestampInput
|
||||
timestamp={typeof draft.created_at === "number" ? draft.created_at : undefined}
|
||||
onChange={(ts) => onChange({ ...draft, created_at: ts })}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Heading size="sm">Tags</Heading>
|
||||
<IconButton
|
||||
size="xs"
|
||||
ml="auto"
|
||||
colorScheme="primary"
|
||||
icon={<Plus boxSize={5} />}
|
||||
aria-label="Add Tag"
|
||||
onClick={() => onChange({ ...draft, tags: [...draft.tags, ["tag", "value"]] })}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{draft.tags.map((tag, index) => (
|
||||
<Flex key={index} gap="2" alignItems="center" wrap="wrap">
|
||||
{tag.map((v, i) => (
|
||||
<Input
|
||||
key={i}
|
||||
value={v}
|
||||
onChange={(e) => setTagValue(index, i, e.target.value)}
|
||||
minW={i === 0 ? undefined : "xs"}
|
||||
maxW={i === 0 ? "40" : undefined}
|
||||
fontWeight={i === 0 ? "bold" : undefined}
|
||||
w="auto"
|
||||
flex={1}
|
||||
/>
|
||||
))}
|
||||
<ButtonGroup isAttached ml="auto">
|
||||
<IconButton
|
||||
size="sm"
|
||||
ml="auto"
|
||||
icon={<Plus boxSize={5} />}
|
||||
aria-label="Add Tag"
|
||||
onClick={() => addTagValue(index)}
|
||||
/>
|
||||
<IconButton
|
||||
size="sm"
|
||||
ml="auto"
|
||||
icon={<Minus boxSize={5} />}
|
||||
aria-label="Add Tag"
|
||||
onClick={() => removeTagValue(index)}
|
||||
isDisabled={tag.length === 1}
|
||||
/>
|
||||
<CloseButton onClick={() => removeTag(index)} />
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
@@ -0,0 +1,98 @@
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import { Text, useColorMode } from "@chakra-ui/react";
|
||||
import ReactCodeMirror from "@uiw/react-codemirror";
|
||||
import { EventTemplate } from "nostr-tools";
|
||||
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";
|
||||
import { LooseEventTemplate } from "../../process";
|
||||
|
||||
const EventJsonEditor = memo(
|
||||
({
|
||||
draft,
|
||||
onChange,
|
||||
onRun,
|
||||
}: {
|
||||
draft?: LooseEventTemplate;
|
||||
onChange: (v: EventTemplate) => void;
|
||||
onRun?: () => void;
|
||||
}) => {
|
||||
const getDirectory = useUserSearchDirectoryContext();
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const [value, setValue] = useState(JSON.stringify(draft, null, 2));
|
||||
const [error, setError] = useState<Error>();
|
||||
const handleChange = (v: string) => {
|
||||
try {
|
||||
setError(undefined);
|
||||
setValue(v);
|
||||
|
||||
const json = JSON.parse(v);
|
||||
if (json.content === undefined) throw new Error("Missing content");
|
||||
if (json.created_at === undefined) throw new Error("Missing created_at");
|
||||
if (json.kind === undefined) throw new Error("Missing kind");
|
||||
if (!Array.isArray(json.tags)) throw new Error("Missing tags");
|
||||
|
||||
onChange(json);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) setError(error);
|
||||
}
|
||||
};
|
||||
|
||||
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={handleChange}
|
||||
height="100%"
|
||||
lang="json"
|
||||
extensions={extensions}
|
||||
theme={colorMode === "light" ? githubLight : githubDark}
|
||||
/>
|
||||
{error && (
|
||||
<Text fontSize="sm" color="red.500">
|
||||
{error.message}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default EventJsonEditor;
|
@@ -0,0 +1,178 @@
|
||||
import { memo, ReactNode } from "react";
|
||||
import {
|
||||
Flex,
|
||||
FlexProps,
|
||||
IconButton,
|
||||
Input,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Select,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { EventTemplate, NostrEvent } from "nostr-tools";
|
||||
|
||||
import { EnumVar, PubkeyVar, StringVar, TimestampVar, Variable } from "../../process";
|
||||
import TimestampInput from "../datetime-input";
|
||||
import PuzzlePiece01 from "../../../../../components/icons/puzzle-piece-01";
|
||||
import UserAutocomplete from "../../../../../components/user-autocomplete";
|
||||
|
||||
function EnumVariableRow({
|
||||
variable,
|
||||
onChange,
|
||||
}: {
|
||||
variable: EnumVar;
|
||||
onChange: (name: string, value: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Select value={variable.value} size="sm" rounded="md" onChange={(e) => onChange(variable.name, e.target.value)}>
|
||||
{variable.options?.map((opts) => (
|
||||
<option value={opts} key={opts}>
|
||||
{opts}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
function StringVariableRow({
|
||||
variable,
|
||||
onChange,
|
||||
}: {
|
||||
variable: StringVar;
|
||||
onChange: (name: string, value: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Input
|
||||
type={variable.input || "text"}
|
||||
value={variable.value}
|
||||
placeholder={variable.placeholder}
|
||||
onChange={(e) => onChange(variable.name, e.target.value)}
|
||||
size="sm"
|
||||
rounded="md"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PubkeyVariableRow({
|
||||
variable,
|
||||
onChange,
|
||||
}: {
|
||||
variable: PubkeyVar;
|
||||
onChange: (name: string, value: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<UserAutocomplete
|
||||
value={variable.value}
|
||||
onChange={(e) => onChange(variable.name, e.target.value)}
|
||||
size="sm"
|
||||
rounded="md"
|
||||
hex
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TimestampVariableRow({
|
||||
variable,
|
||||
onChange,
|
||||
}: {
|
||||
variable: TimestampVar;
|
||||
onChange: (name: string, value: string) => void;
|
||||
}) {
|
||||
const ts = parseInt(variable.value);
|
||||
|
||||
return (
|
||||
<TimestampInput
|
||||
timestamp={Number.isFinite(ts) ? ts : undefined}
|
||||
onChange={(ts) => onChange(variable.name, String(ts))}
|
||||
size="sm"
|
||||
rounded="md"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const VariableRow = memo(
|
||||
({
|
||||
variable,
|
||||
onValueChange,
|
||||
onTypeChange,
|
||||
}: {
|
||||
variable: Variable;
|
||||
onValueChange: (name: string, value: string) => void;
|
||||
onTypeChange: (name: string, type: string) => void;
|
||||
}) => {
|
||||
let content: ReactNode = null;
|
||||
switch (variable.type) {
|
||||
case "enum":
|
||||
content = <EnumVariableRow variable={variable} onChange={onValueChange} />;
|
||||
break;
|
||||
case "string":
|
||||
content = <StringVariableRow variable={variable} onChange={onValueChange} />;
|
||||
break;
|
||||
case "pubkey":
|
||||
content = <PubkeyVariableRow variable={variable} onChange={onValueChange} />;
|
||||
break;
|
||||
case "timestamp":
|
||||
content = <TimestampVariableRow variable={variable} onChange={onValueChange} />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap="2" borderWidth="1px" rounded="md" p="2" alignItems="center">
|
||||
<Text fontWeight="bold" flexShrink={0} mr="2">
|
||||
{variable.name}
|
||||
</Text>
|
||||
{content}
|
||||
<Menu>
|
||||
<MenuButton as={IconButton} size="sm" aria-label="Options" icon={<PuzzlePiece01 />} variant="outline" />
|
||||
<MenuList>
|
||||
<MenuItem onClick={() => onTypeChange(variable.name, "string")}>String</MenuItem>
|
||||
<MenuItem onClick={() => onTypeChange(variable.name, "pubkey")}>Pubkey</MenuItem>
|
||||
<MenuItem onClick={() => onTypeChange(variable.name, "timestamp")}>Timestamp</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default function VariableEditor({
|
||||
variables,
|
||||
onChange,
|
||||
...props
|
||||
}: {
|
||||
draft?: NostrEvent | EventTemplate;
|
||||
variables: Variable[];
|
||||
onChange: (variables: Variable[]) => void;
|
||||
} & Omit<FlexProps, "children" | "onChange">) {
|
||||
const setVariableValue = (name: string, value: string) => {
|
||||
onChange(
|
||||
variables.map((v) => {
|
||||
if (v.name === name) return { ...v, value };
|
||||
return v;
|
||||
}),
|
||||
);
|
||||
};
|
||||
const setVariableType = (name: string, type: string) => {
|
||||
onChange(
|
||||
variables.map((v) => {
|
||||
if (v.name === name) return { ...v, type } as Variable;
|
||||
return v;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2" {...props}>
|
||||
{variables.map((variable) => (
|
||||
<VariableRow
|
||||
key={variable.name}
|
||||
variable={variable}
|
||||
onValueChange={setVariableValue}
|
||||
onTypeChange={setVariableType}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
@@ -1,62 +0,0 @@
|
||||
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;
|
@@ -1,8 +1,9 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Code,
|
||||
Divider,
|
||||
Flex,
|
||||
Heading,
|
||||
Modal,
|
||||
@@ -12,78 +13,101 @@ import {
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Select,
|
||||
Spacer,
|
||||
Switch,
|
||||
Text,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { EventTemplate, NostrEvent, UnsignedEvent, getEventHash, verifyEvent } from "nostr-tools";
|
||||
import dayjs from "dayjs";
|
||||
import { NostrEvent, UnsignedEvent, verifyEvent } from "nostr-tools";
|
||||
|
||||
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 { processEvent } from "./process";
|
||||
import { WritingIcon } from "../../../components/icons";
|
||||
import EventJsonEditor from "./components/json-editor";
|
||||
import { getVariables, LooseEventTemplate, processEvent, Variable } from "./process";
|
||||
import { EditIcon, 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";
|
||||
import { RelayUrlInput } from "../../../components/relay-url-input";
|
||||
import { TEMPLATES } from "./templates";
|
||||
import RequireCurrentAccount from "../../../providers/route/require-current-account";
|
||||
import VariableEditor from "./components/variable-editor";
|
||||
import EventTemplateEditor from "./components/event-template-editor";
|
||||
import useRouteStateValue from "../../../hooks/use-route-state-value";
|
||||
import { cloneEvent } from "../../../helpers/nostr/event";
|
||||
|
||||
export default function EventPublisherView() {
|
||||
function EventPublisherPage({ initDraft }: { initDraft?: LooseEventTemplate }) {
|
||||
const toast = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { requestSignature } = useSigningContext();
|
||||
const publish = usePublishEvent();
|
||||
const account = useCurrentAccount();
|
||||
const account = useCurrentAccount()!;
|
||||
const customRelay = useDisclosure();
|
||||
|
||||
const editor = useDisclosure({ defaultIsOpen: true });
|
||||
const editRaw = useDisclosure();
|
||||
|
||||
const [customRelayURL, setCustomRelayURL] = useState("");
|
||||
|
||||
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 [variables, setVariables] = useState<Variable[]>(initDraft ? [] : TEMPLATES[0].variables);
|
||||
const [draft, setDraft] = useState<LooseEventTemplate>(initDraft || TEMPLATES[0].template());
|
||||
const [finalized, setFinalized] = useState<UnsignedEvent>();
|
||||
|
||||
// update the variables based on the template
|
||||
useEffect(() => {
|
||||
if (draft) {
|
||||
const variableNames = getVariables(draft);
|
||||
|
||||
setVariables((current) => {
|
||||
return variableNames.map((name) => {
|
||||
return current.find((v) => v.name === name) || { type: "string", value: "", name };
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [draft]);
|
||||
|
||||
const [processed, setProcessed] = useState<UnsignedEvent>();
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (!draft) return;
|
||||
setProcessed(processEvent(draft, variables, account));
|
||||
} catch (error) {}
|
||||
}, [draft, account, variables]);
|
||||
|
||||
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);
|
||||
const event = processEvent(draft, variables, account);
|
||||
setFinalized(event);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
const sign = async () => {
|
||||
if (!draft) return;
|
||||
if (!finalized) return;
|
||||
try {
|
||||
setDraft(await requestSignature(draft));
|
||||
setFinalized(await requestSignature(finalized));
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
const publishDraft = async () => {
|
||||
if (!draft || !(draft as NostrEvent).sig) return;
|
||||
if (!finalized || !(finalized as NostrEvent).sig) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
const valid = verifyEvent(draft as NostrEvent);
|
||||
if (!valid) throw new Error("Invalid event");
|
||||
if (customRelayURL) {
|
||||
await publish("Custom Event", draft, [customRelayURL], true, true);
|
||||
await publish("Custom Event", finalized, [customRelayURL], true, true);
|
||||
} else {
|
||||
await publish("Custom Event", draft);
|
||||
await publish("Custom Event", finalized);
|
||||
}
|
||||
setDraft(undefined);
|
||||
setFinalized(undefined);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
@@ -92,37 +116,46 @@ export default function EventPublisherView() {
|
||||
|
||||
const yolo = async () => {
|
||||
try {
|
||||
if (!account) return;
|
||||
|
||||
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 event = await requestSignature(processEvent(draft, variables, account));
|
||||
|
||||
const valid = verifyEvent(event);
|
||||
if (!valid) throw new Error("Invalid event");
|
||||
if (customRelayURL) {
|
||||
await publish("Custom Event", draft, [customRelayURL], true, true);
|
||||
await publish("Custom Event", event, [customRelayURL], true, true);
|
||||
} else {
|
||||
await publish("Custom Event", draft);
|
||||
await publish("Custom Event", event);
|
||||
}
|
||||
setDraft(undefined);
|
||||
setFinalized(undefined);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const selectTemplate = (name: string) => {
|
||||
const template = TEMPLATES.find((t) => t.name === name);
|
||||
if (template) {
|
||||
setVariables(template.variables);
|
||||
setDraft(template.template());
|
||||
if (template.variables.length > 0) editor.onClose();
|
||||
else editor.onOpen();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<VerticalPageLayout>
|
||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||
<BackButton size="sm" />
|
||||
<BackButton />
|
||||
<Heading size="md">Event Publisher</Heading>
|
||||
<Switch size="sm" isChecked={customRelay.isOpen} onChange={customRelay.onToggle}>
|
||||
Publish to Relay
|
||||
</Switch>
|
||||
{customRelay.isOpen && (
|
||||
<RelayUrlInput
|
||||
size="sm"
|
||||
borderRadius="md"
|
||||
w="xs"
|
||||
value={customRelayURL}
|
||||
@@ -130,17 +163,64 @@ export default function EventPublisherView() {
|
||||
/>
|
||||
)}
|
||||
<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">
|
||||
<Button colorScheme="primary" onClick={submitEvent} leftIcon={<Play />}>
|
||||
Publish
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
|
||||
<EventEditor value={value} onChange={(v) => setValue(v)} onRun={submitEvent} />
|
||||
<Flex direction={{ base: "column", xl: "row" }} gap="2">
|
||||
<Flex direction="column" gap="2" flex={1} overflow="hidden">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Text fontWeight="bold">Template</Text>
|
||||
<Select onChange={(e) => selectTemplate(e.target.value)} w="auto">
|
||||
{TEMPLATES.map((template) => (
|
||||
<option value={template.name}>{template.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
<Spacer />
|
||||
<ButtonGroup size="sm">
|
||||
{editor.isOpen && (
|
||||
<Button onClick={editRaw.onToggle} colorScheme={editRaw.isOpen ? "primary" : undefined}>
|
||||
Raw
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={editor.onToggle}
|
||||
colorScheme={editor.isOpen ? "primary" : undefined}
|
||||
leftIcon={<EditIcon />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
|
||||
{editor.isOpen &&
|
||||
(editRaw.isOpen ? (
|
||||
<EventJsonEditor draft={draft} onChange={setDraft} onRun={submitEvent} />
|
||||
) : (
|
||||
<EventTemplateEditor draft={draft} onChange={setDraft} />
|
||||
))}
|
||||
|
||||
<Flex gap="2">
|
||||
<Heading size="md" mt="4">
|
||||
Variables
|
||||
</Heading>
|
||||
</Flex>
|
||||
<VariableEditor variables={variables} onChange={(v) => setVariables(v)} />
|
||||
</Flex>
|
||||
|
||||
<Divider hideFrom="xl" />
|
||||
|
||||
<Flex flex={1} direction="column" gap="2" overflow="hidden">
|
||||
<Code whiteSpace="pre" overflow="auto" p="2">
|
||||
{JSON.stringify(processed, null, 2)}
|
||||
</Code>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</VerticalPageLayout>
|
||||
{draft && (
|
||||
<Modal isOpen onClose={() => setDraft(undefined)} size="2xl">
|
||||
{finalized && (
|
||||
<Modal isOpen onClose={() => setFinalized(undefined)} size="2xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader p="4">Publish Event</ModalHeader>
|
||||
@@ -150,30 +230,30 @@ export default function EventPublisherView() {
|
||||
1. Event ID
|
||||
</Heading>
|
||||
<Code w="full" p="2" overflow="auto">
|
||||
{(draft as NostrEvent).id}
|
||||
{(finalized 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}
|
||||
{(finalized as NostrEvent).pubkey}
|
||||
</Code>
|
||||
<UserAvatar pubkey={(draft as NostrEvent).pubkey} />
|
||||
<UserAvatar pubkey={(finalized 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}
|
||||
{(finalized as NostrEvent).sig}
|
||||
</Code>
|
||||
<Button leftIcon={<WritingIcon boxSize={5} />} flexShrink={0} onClick={sign} ml="auto">
|
||||
Sign
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{(draft as NostrEvent).sig && (
|
||||
{(finalized as NostrEvent).sig && (
|
||||
<Button w="full" colorScheme="primary" mt="2" isLoading={loading} onClick={publishDraft}>
|
||||
Publish
|
||||
</Button>
|
||||
@@ -181,7 +261,7 @@ export default function EventPublisherView() {
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button mr={2} onClick={() => setDraft(undefined)}>
|
||||
<Button mr={2} onClick={() => setFinalized(undefined)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="primary" onClick={yolo} isLoading={loading}>
|
||||
@@ -194,3 +274,19 @@ export default function EventPublisherView() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EventPublisherView() {
|
||||
let { value: draft } = useRouteStateValue<NostrEvent>("draft");
|
||||
|
||||
if (draft && draft.sig) {
|
||||
draft = { ...draft };
|
||||
// @ts-ignore
|
||||
delete draft.sig;
|
||||
}
|
||||
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<EventPublisherPage initDraft={draft} />
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
}
|
||||
|
@@ -1,10 +1,87 @@
|
||||
import { EventTemplate, UnsignedEvent } from "nostr-tools";
|
||||
import { processDateString } from "../event-console/process";
|
||||
import { EventTemplate, getEventHash, NostrEvent, UnsignedEvent } from "nostr-tools";
|
||||
import Handlebars from "handlebars";
|
||||
|
||||
import { processDateString } from "../event-console/process";
|
||||
import { Account } from "../../../classes/accounts/account";
|
||||
import { InputProps } from "@chakra-ui/react";
|
||||
|
||||
type BaseVariable<T, V> = {
|
||||
type: T;
|
||||
name: string;
|
||||
value: string;
|
||||
} & Partial<V>;
|
||||
export type StringVar = BaseVariable<
|
||||
"string",
|
||||
{
|
||||
input: InputProps["type"];
|
||||
placeholder: string;
|
||||
}
|
||||
>;
|
||||
export type EnumVar = BaseVariable<
|
||||
"enum",
|
||||
{
|
||||
type: "enum";
|
||||
options: string[];
|
||||
}
|
||||
>;
|
||||
export type TimestampVar = BaseVariable<"timestamp", {}>;
|
||||
export type PubkeyVar = BaseVariable<"pubkey", {}>;
|
||||
|
||||
export type Variable = StringVar | EnumVar | TimestampVar | PubkeyVar;
|
||||
|
||||
export type LooseEventTemplate = Omit<UnsignedEvent | NostrEvent | EventTemplate, "created_at"> & {
|
||||
created_at: number | string | undefined;
|
||||
};
|
||||
|
||||
function getVariablesFromString(str: string) {
|
||||
if (str.length === 0) return;
|
||||
|
||||
const matches = str.matchAll(/{{([^{}]+)}}/g);
|
||||
return Array.from(matches).map((match) => match[1].trim());
|
||||
}
|
||||
function setVariablesInString(str: string, variables: Record<string, any>) {
|
||||
if (!str.includes("{{")) return str;
|
||||
return Handlebars.compile(str)(variables);
|
||||
}
|
||||
|
||||
export function getVariables(draft?: LooseEventTemplate) {
|
||||
if (!draft) return [];
|
||||
|
||||
const variables: string[] = [];
|
||||
|
||||
const add = (vars?: string[]) => {
|
||||
if (!vars) return;
|
||||
for (const v of vars) {
|
||||
if (!variables.includes(v)) variables.push(v);
|
||||
}
|
||||
};
|
||||
|
||||
add(getVariablesFromString(draft.content));
|
||||
for (const tag of draft.tags) {
|
||||
for (const v of tag) {
|
||||
add(getVariablesFromString(v));
|
||||
}
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
export function processEvent(draft: LooseEventTemplate, variables: Variable[], account: Account): UnsignedEvent {
|
||||
const event = { ...draft } as UnsignedEvent;
|
||||
|
||||
const vars: Record<string, string> = variables.reduce((dir, v) => ({ ...dir, [v.name]: v.value }), {});
|
||||
|
||||
event.content = setVariablesInString(event.content, vars);
|
||||
event.tags = event.tags.map((tag) => tag.map((v) => setVariablesInString(v, vars)));
|
||||
|
||||
export function processEvent(event: UnsignedEvent): UnsignedEvent {
|
||||
if (typeof event.created_at === "string") {
|
||||
event.created_at = processDateString(event.created_at);
|
||||
}
|
||||
|
||||
return event;
|
||||
event.pubkey = account.pubkey;
|
||||
|
||||
// @ts-expect-error
|
||||
event.id = getEventHash(event);
|
||||
|
||||
return event as UnsignedEvent;
|
||||
}
|
||||
|
38
src/views/tools/event-publisher/templates.ts
Normal file
38
src/views/tools/event-publisher/templates.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { kinds } from "nostr-tools";
|
||||
import { LooseEventTemplate, Variable } from "./process";
|
||||
|
||||
export const TEMPLATES: { name: string; variables: Variable[]; template: () => LooseEventTemplate }[] = [
|
||||
{
|
||||
name: "Short Text Note",
|
||||
variables: [],
|
||||
template: () => ({
|
||||
kind: kinds.ShortTextNote,
|
||||
content: "Hello World",
|
||||
created_at: "now",
|
||||
tags: [],
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Live Stream",
|
||||
variables: [
|
||||
{ type: "enum", name: "status", value: "live", options: ["live", "ended"] },
|
||||
{ type: "timestamp", name: "starts", value: "" },
|
||||
{ type: "string", name: "streaming", input: "url", placeholder: "https://example.com/stream.m3u8", value: "" },
|
||||
{ type: "string", name: "image", input: "url", placeholder: "https://example.com/stream-image.png", value: "" },
|
||||
],
|
||||
template: () => ({
|
||||
content: "",
|
||||
created_at: "now",
|
||||
kind: kinds.LiveEvent,
|
||||
tags: [
|
||||
["d", "{{id}}"],
|
||||
["title", "{{title}}"],
|
||||
["summary", "{{summary}}"],
|
||||
["streaming", "{{streaming}}"],
|
||||
["status", "{{status}}"],
|
||||
["starts", "{{starts}}"],
|
||||
["image", "{{image}}"],
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
32
yarn.lock
32
yarn.lock
@@ -4825,6 +4825,18 @@ grapheme-splitter@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
|
||||
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
|
||||
|
||||
handlebars@^4.7.8:
|
||||
version "4.7.8"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9"
|
||||
integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
neo-async "^2.6.2"
|
||||
source-map "^0.6.1"
|
||||
wordwrap "^1.0.0"
|
||||
optionalDependencies:
|
||||
uglify-js "^3.1.4"
|
||||
|
||||
hard-rejection@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
|
||||
@@ -6150,6 +6162,11 @@ minimist-options@^4.0.2:
|
||||
is-plain-obj "^1.1.0"
|
||||
kind-of "^6.0.3"
|
||||
|
||||
minimist@^1.2.5:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
mixme@^0.5.1:
|
||||
version "0.5.10"
|
||||
resolved "https://registry.yarnpkg.com/mixme/-/mixme-0.5.10.tgz#d653b2984b75d9018828f1ea333e51717ead5f51"
|
||||
@@ -6204,6 +6221,11 @@ nearley@^2.20.1:
|
||||
railroad-diagrams "^1.0.0"
|
||||
randexp "0.4.6"
|
||||
|
||||
neo-async@^2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||
|
||||
ngeohash@^0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/ngeohash/-/ngeohash-0.6.3.tgz#10b1e80be5488262ec95c56cf2dbb6c45fbdf245"
|
||||
@@ -7835,6 +7857,11 @@ typo-js@*:
|
||||
resolved "https://registry.yarnpkg.com/typo-js/-/typo-js-1.2.4.tgz#0e009c289a966dd51dc80a75580289a381cc607f"
|
||||
integrity sha512-Oy/k+tFle5NAA3J/yrrYGfvEnPVrDZ8s8/WCwjUE75k331QyKIsFss7byQ/PzBmXLY6h1moRnZbnaxWBe3I3CA==
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.19.3"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f"
|
||||
integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==
|
||||
|
||||
unbox-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
|
||||
@@ -8134,6 +8161,11 @@ which@^1.2.9:
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
wordwrap@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
|
||||
|
||||
workbox-background-sync@7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-7.0.0.tgz#2b84b96ca35fec976e3bd2794b70e4acec46b3a5"
|
||||
|
Reference in New Issue
Block a user