mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-11 05:09:36 +02:00
add blossom media upload option
This commit is contained in:
parent
ed1dc8ded6
commit
8d46272558
5
.changeset/modern-fishes-own.md
Normal file
5
.changeset/modern-fishes-own.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add blossom media upload option
|
@ -38,6 +38,8 @@
|
||||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
||||
"bech32": "^2.0.0",
|
||||
"blossom-client": "^0.4.0",
|
||||
"blossom-drive-client": "^0.1.0",
|
||||
"blurhash": "^2.0.5",
|
||||
"chart.js": "^4.4.1",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
|
@ -73,6 +73,7 @@ import CacheRelayView from "./views/relays/cache";
|
||||
import RelaySetView from "./views/relays/relay-set";
|
||||
import AppRelays from "./views/relays/app";
|
||||
import MailboxesView from "./views/relays/mailboxes";
|
||||
import MediaServersView from "./views/relays/media-servers";
|
||||
import NIP05RelaysView from "./views/relays/nip05";
|
||||
import ContactListRelaysView from "./views/relays/contact-list";
|
||||
import UserDMsTab from "./views/user/dms";
|
||||
@ -271,6 +272,7 @@ const router = createHashRouter([
|
||||
{ path: "app", element: <AppRelays /> },
|
||||
{ path: "cache", element: <CacheRelayView /> },
|
||||
{ path: "mailboxes", element: <MailboxesView /> },
|
||||
{ path: "media-servers", element: <MediaServersView /> },
|
||||
{ path: "nip05", element: <NIP05RelaysView /> },
|
||||
{ path: "contacts", element: <ContactListRelaysView /> },
|
||||
{ path: "sets", element: <BrowseRelaySetsView /> },
|
||||
|
@ -84,7 +84,7 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent
|
||||
<ModalHeader p="4">{event.id}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody p="0">
|
||||
<Accordion allowToggle>
|
||||
<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)} />
|
||||
|
@ -95,6 +95,7 @@ export const ChevronRightIcon = ChevronRight;
|
||||
|
||||
export const LightningIcon = Zap;
|
||||
export const RelayIcon = Modem02;
|
||||
export const MediaServerIcon = Database01;
|
||||
export const BroadcastEventIcon = Share07;
|
||||
export const ShareIcon = Share07;
|
||||
export const PinIcon = Pin01;
|
||||
|
18
src/components/media-server/media-server-favicon.tsx
Normal file
18
src/components/media-server/media-server-favicon.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { useMemo } from "react";
|
||||
import { Avatar, AvatarProps } from "@chakra-ui/react";
|
||||
|
||||
import { MediaServerIcon } from "../icons";
|
||||
|
||||
export type RelayFaviconProps = Omit<AvatarProps, "src"> & {
|
||||
server: string;
|
||||
};
|
||||
export default function MediaServerFavicon({ server, ...props }: RelayFaviconProps) {
|
||||
const url = useMemo(() => {
|
||||
const url = new URL(server);
|
||||
url.protocol = "https:";
|
||||
url.pathname = "/favicon.ico";
|
||||
return url.toString();
|
||||
}, [server]);
|
||||
|
||||
return <Avatar src={url} icon={<MediaServerIcon />} overflow="hidden" {...props} />;
|
||||
}
|
@ -215,7 +215,7 @@ export default function PostModal({
|
||||
onChange={onFileInputChange}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<UploadImageIcon />}
|
||||
icon={<UploadImageIcon boxSize={6} />}
|
||||
aria-label="Upload Image"
|
||||
title="Upload Image"
|
||||
onClick={() => imageUploadRef.current?.click()}
|
||||
|
23
src/helpers/media-upload/blossom.ts
Normal file
23
src/helpers/media-upload/blossom.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { safeUrl } from "../parse";
|
||||
import { BlobDescriptor, BlossomClient, Signer } from "blossom-client";
|
||||
|
||||
export function getServersFromEvent(event: NostrEvent) {
|
||||
return event.tags
|
||||
.filter((t) => t[0] === "r")
|
||||
.map((t) => safeUrl(t[1]))
|
||||
.filter(Boolean) as string[];
|
||||
}
|
||||
|
||||
export async function uploadFileToServers(servers: string[], file: File, signer: Signer) {
|
||||
const results: BlobDescriptor[] = [];
|
||||
|
||||
const auth = await BlossomClient.getUploadAuth(file, signer);
|
||||
for (const server of servers) {
|
||||
try {
|
||||
results.push(await BlossomClient.uploadBlob(server, file, auth));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return results[0];
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { nip98 } from "nostr-tools";
|
||||
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
type NostrBuildResponse = {
|
||||
status: "success" | "error";
|
@ -1,10 +1,14 @@
|
||||
import { ChangeEventHandler, ClipboardEventHandler, MutableRefObject, useCallback, useState } from "react";
|
||||
import { useToast } from "@chakra-ui/react";
|
||||
|
||||
import { nostrBuildUploadImage } from "../helpers/nostr-build";
|
||||
import { nostrBuildUploadImage } from "../helpers/media-upload/nostr-build";
|
||||
import { RefType } from "../components/magic-textarea";
|
||||
import { useSigningContext } from "../providers/global/signing-provider";
|
||||
import { UseFormGetValues, UseFormSetValue } from "react-hook-form";
|
||||
import useAppSettings from "./use-app-settings";
|
||||
import useUsersMediaServers from "./use-user-media-servers";
|
||||
import { getServersFromEvent, uploadFileToServers } from "../helpers/media-upload/blossom";
|
||||
import useCurrentAccount from "./use-current-account";
|
||||
|
||||
export function useTextAreaUploadFileWithForm(
|
||||
ref: MutableRefObject<RefType | null>,
|
||||
@ -25,45 +29,55 @@ export default function useTextAreaUploadFile(
|
||||
setText: (text: string) => void,
|
||||
) {
|
||||
const toast = useToast();
|
||||
const account = useCurrentAccount();
|
||||
const { mediaUploadService } = useAppSettings();
|
||||
const mediaServers = useUsersMediaServers(account?.pubkey);
|
||||
const { requestSignature } = useSigningContext();
|
||||
|
||||
const insertURL = useCallback(
|
||||
(url: string) => {
|
||||
const content = getText();
|
||||
const position = ref.current?.getCaretPosition();
|
||||
if (position !== undefined) {
|
||||
let inject = url;
|
||||
|
||||
// add a space before
|
||||
if (position >= 1 && content.slice(position - 1, position) !== " ") inject = " " + inject;
|
||||
// add a space after
|
||||
if (position < content.length && content.slice(position, position + 1) !== " ") inject = inject + " ";
|
||||
|
||||
setText(content.slice(0, position) + inject + content.slice(position));
|
||||
} else {
|
||||
let inject = url;
|
||||
|
||||
// add a space before if there isn't one
|
||||
if (content.slice(content.length - 1) !== " ") inject = " " + inject;
|
||||
|
||||
setText(content + inject + " ");
|
||||
}
|
||||
},
|
||||
[setText, getText],
|
||||
);
|
||||
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const uploadFile = useCallback(
|
||||
async (file: File) => {
|
||||
setUploading(true);
|
||||
try {
|
||||
if (!(file.type.includes("image") || file.type.includes("video") || file.type.includes("audio")))
|
||||
throw new Error("Unsupported file type");
|
||||
|
||||
setUploading(true);
|
||||
|
||||
const response = await nostrBuildUploadImage(file, requestSignature);
|
||||
const imageUrl = response.url;
|
||||
|
||||
const content = getText();
|
||||
const position = ref.current?.getCaretPosition();
|
||||
if (position !== undefined) {
|
||||
let inject = imageUrl;
|
||||
|
||||
// add a space before
|
||||
if (position >= 1 && content.slice(position - 1, position) !== " ") inject = " " + inject;
|
||||
// add a space after
|
||||
if (position < content.length && content.slice(position, position + 1) !== " ") inject = inject + " ";
|
||||
|
||||
setText(content.slice(0, position) + inject + content.slice(position));
|
||||
} else {
|
||||
let inject = imageUrl;
|
||||
|
||||
// add a space before if there isn't one
|
||||
if (content.slice(content.length - 1) !== " ") inject = " " + inject;
|
||||
|
||||
setText(content + inject + " ");
|
||||
if (mediaUploadService === "nostr.build") {
|
||||
const response = await nostrBuildUploadImage(file, requestSignature);
|
||||
const imageUrl = response.url;
|
||||
insertURL(imageUrl);
|
||||
} else if (mediaUploadService === "blossom" && mediaServers) {
|
||||
const blob = await uploadFileToServers(getServersFromEvent(mediaServers), file, requestSignature);
|
||||
insertURL(blob.url);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
setUploading(false);
|
||||
},
|
||||
[setText, getText, toast, setUploading],
|
||||
[insertURL, toast, setUploading, mediaServers, mediaUploadService],
|
||||
);
|
||||
|
||||
const onFileInputChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||
|
10
src/hooks/use-user-media-servers.ts
Normal file
10
src/hooks/use-user-media-servers.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import replaceableEventsService, { RequestOptions } from "../services/replaceable-events";
|
||||
import { useReadRelays } from "./use-client-relays";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export default function useUsersMediaServers(pubkey?: string, additionalRelays?: string[], opts?: RequestOptions) {
|
||||
const readRelays = useReadRelays(additionalRelays);
|
||||
const sub = pubkey ? replaceableEventsService.requestEvent(readRelays, 10063, pubkey, undefined, opts) : undefined;
|
||||
const value = useSubject(sub);
|
||||
return value;
|
||||
}
|
@ -34,8 +34,12 @@ export type AppSettingsV4 = Omit<AppSettingsV3, "version"> & { version: 4; loadO
|
||||
export type AppSettingsV5 = Omit<AppSettingsV4, "version"> & { version: 5; hideUsernames: boolean };
|
||||
export type AppSettingsV6 = Omit<AppSettingsV5, "version"> & { version: 6; noteDifficulty: number | null };
|
||||
export type AppSettingsV7 = Omit<AppSettingsV6, "version"> & { version: 7; autoDecryptDMs: boolean };
|
||||
export type AppSettingsV8 = Omit<AppSettingsV7, "version"> & {
|
||||
version: 7;
|
||||
mediaUploadService: "nostr.build" | "blossom";
|
||||
};
|
||||
|
||||
export type AppSettings = AppSettingsV7;
|
||||
export type AppSettings = AppSettingsV8;
|
||||
|
||||
export const defaultSettings: AppSettings = {
|
||||
version: 7,
|
||||
@ -55,6 +59,7 @@ export const defaultSettings: AppSettings = {
|
||||
autoDecryptDMs: false,
|
||||
|
||||
quickReactions: ["🤙", "❤️", "🤣", "😍", "🔥"],
|
||||
mediaUploadService: "nostr.build",
|
||||
|
||||
autoPayWithWebLN: true,
|
||||
customZapAmounts: "50,200,500,1000,2000,5000",
|
||||
|
@ -30,7 +30,7 @@ import UserAvatar from "../../../components/user/user-avatar";
|
||||
import UserLink from "../../../components/user/user-link";
|
||||
import { TrashIcon } from "../../../components/icons";
|
||||
import Upload01 from "../../../components/icons/upload-01";
|
||||
import { nostrBuildUploadImage } from "../../../helpers/nostr-build";
|
||||
import { nostrBuildUploadImage } from "../../../helpers/media-upload/nostr-build";
|
||||
import { useSigningContext } from "../../../providers/global/signing-provider";
|
||||
import { RelayUrlInput } from "../../../components/relay-url-input";
|
||||
import { RelayFavicon } from "../../../components/relay-favicon";
|
||||
|
@ -1,30 +1,17 @@
|
||||
import { Button, Code, Flex, Heading, Link, Spinner, Text } from "@chakra-ui/react";
|
||||
import { Button, Flex, Heading, Link, Spinner, Text } from "@chakra-ui/react";
|
||||
import BackButton from "../../../components/router/back-button";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { RelayFavicon } from "../../../components/relay-favicon";
|
||||
import useUserContactRelays from "../../../hooks/use-user-contact-relays";
|
||||
import { CheckIcon, InboxIcon, OutboxIcon } from "../../../components/icons";
|
||||
import { CheckIcon } from "../../../components/icons";
|
||||
import { useCallback, useState } from "react";
|
||||
import useCacheForm from "../../../hooks/use-cache-form";
|
||||
import useUserContactList from "../../../hooks/use-user-contact-list";
|
||||
import { cloneEvent } from "../../../helpers/nostr/event";
|
||||
import { EventTemplate } from "nostr-tools";
|
||||
import dayjs from "dayjs";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
|
||||
function RelayItem({ url }: { url: string }) {
|
||||
return (
|
||||
<Flex gap="2" alignItems="center">
|
||||
<RelayFavicon relay={url} size="sm" />
|
||||
<Link as={RouterLink} to={`/r/${encodeURIComponent(url)}`} isTruncated>
|
||||
{url}
|
||||
</Link>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ContactListRelaysView() {
|
||||
const account = useCurrentAccount();
|
||||
const contacts = useUserContactList(account?.pubkey);
|
||||
|
@ -13,6 +13,7 @@ import Mail02 from "../../components/icons/mail-02";
|
||||
import { useUserDNSIdentity } from "../../hooks/use-user-dns-identity";
|
||||
import useUserContactRelays from "../../hooks/use-user-contact-relays";
|
||||
import UserSquare from "../../components/icons/user-square";
|
||||
import Image01 from "../../components/icons/image-01";
|
||||
|
||||
export default function RelaysView() {
|
||||
const account = useCurrentAccount();
|
||||
@ -49,15 +50,26 @@ export default function RelaysView() {
|
||||
Cache Relay
|
||||
</Button>
|
||||
{account && (
|
||||
<Button
|
||||
variant="outline"
|
||||
as={RouterLink}
|
||||
to="/relays/mailboxes"
|
||||
leftIcon={<Mail02 boxSize={6} />}
|
||||
colorScheme={location.pathname === "/relays/mailboxes" ? "primary" : undefined}
|
||||
>
|
||||
Mailboxes
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
as={RouterLink}
|
||||
to="/relays/mailboxes"
|
||||
leftIcon={<Mail02 boxSize={6} />}
|
||||
colorScheme={location.pathname === "/relays/mailboxes" ? "primary" : undefined}
|
||||
>
|
||||
Mailboxes
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
as={RouterLink}
|
||||
to="/relays/media-servers"
|
||||
leftIcon={<Image01 boxSize={6} />}
|
||||
colorScheme={location.pathname === "/relays/media-servers" ? "primary" : undefined}
|
||||
>
|
||||
Media Servers
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{nip05 && (
|
||||
<Button
|
||||
|
@ -1,36 +1,22 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { Flex, Heading, IconButton, Link, Text } from "@chakra-ui/react";
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||
import RequireCurrentAccount from "../../../providers/route/require-current-account";
|
||||
import useUserMailboxes from "../../../hooks/use-user-mailboxes";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { InboxIcon, OutboxIcon } from "../../../components/icons";
|
||||
import { RelayUrlInput } from "../../../components/relay-url-input";
|
||||
import { RelayFavicon } from "../../../components/relay-favicon";
|
||||
import MediaServerFavicon from "../../../components/media-server/media-server-favicon";
|
||||
import { RelayMode } from "../../../classes/relay";
|
||||
import { useCallback } from "react";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { addRelayModeToMailbox, removeRelayModeFromMailbox } from "../../../helpers/nostr/mailbox";
|
||||
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { safeRelayUrl } from "../../../helpers/relay";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
import { COMMON_CONTACT_RELAY } from "../../../const";
|
||||
import BackButton from "../../../components/router/back-button";
|
||||
import { addRelayModeToMailbox, removeRelayModeFromMailbox } from "../../../helpers/nostr/mailbox";
|
||||
import AddRelayForm from "../app/add-relay-form";
|
||||
import DebugEventButton from "../../../components/debug-modal/debug-event-button";
|
||||
|
||||
function RelayLine({ relay, mode, list }: { relay: string; mode: RelayMode; list?: NostrEvent }) {
|
||||
const publish = usePublishEvent();
|
||||
@ -41,7 +27,7 @@ function RelayLine({ relay, mode, list }: { relay: string; mode: RelayMode; list
|
||||
|
||||
return (
|
||||
<Flex key={relay} gap="2" alignItems="center" overflow="hidden">
|
||||
<RelayFavicon relay={relay} size="xs" />
|
||||
<MediaServerFavicon server={relay} size="xs" />
|
||||
<Link as={RouterLink} to={`/r/${encodeURIComponent(relay)}`} isTruncated>
|
||||
{relay}
|
||||
</Link>
|
||||
@ -76,6 +62,7 @@ function MailboxesPage() {
|
||||
<Flex gap="2" alignItems="center">
|
||||
<BackButton hideFrom="lg" size="sm" />
|
||||
<Heading size="lg">Mailboxes</Heading>
|
||||
{event && <DebugEventButton event={event} size="sm" ml="auto" />}
|
||||
</Flex>
|
||||
<Text fontStyle="italic" mt="-2">
|
||||
Mailbox relays are a way for other users to find your events, or send you events. they are defined in{" "}
|
||||
|
192
src/views/relays/media-servers/index.tsx
Normal file
192
src/views/relays/media-servers/index.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
CloseButton,
|
||||
Divider,
|
||||
Flex,
|
||||
Heading,
|
||||
Input,
|
||||
Link,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Text,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import RequireCurrentAccount from "../../../providers/route/require-current-account";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import MediaServerFavicon from "../../../components/media-server/media-server-favicon";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
import BackButton from "../../../components/router/back-button";
|
||||
import useUsersMediaServers from "../../../hooks/use-user-media-servers";
|
||||
import DebugEventButton from "../../../components/debug-modal/debug-event-button";
|
||||
import { cloneEvent } from "../../../helpers/nostr/event";
|
||||
import useAppSettings from "../../../hooks/use-app-settings";
|
||||
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
|
||||
import { getServersFromEvent } from "../../../helpers/media-upload/blossom";
|
||||
|
||||
function serversEqual(a: string, b: string) {
|
||||
return new URL(a).hostname === new URL(b).hostname;
|
||||
}
|
||||
|
||||
function MediaServersPage() {
|
||||
const toast = useToast();
|
||||
const account = useCurrentAccount()!;
|
||||
const publish = usePublishEvent();
|
||||
const { mediaUploadService, updateSettings } = useAppSettings();
|
||||
const mediaServers = useUsersMediaServers(account.pubkey, undefined, { alwaysRequest: true, ignoreCache: true });
|
||||
|
||||
const servers = mediaServers ? getServersFromEvent(mediaServers) : [];
|
||||
|
||||
const addServer = async (server: string) => {
|
||||
const draft = cloneEvent(10063, mediaServers);
|
||||
draft.tags = [...draft.tags, ["r", server]];
|
||||
await publish("Add media server", draft);
|
||||
};
|
||||
const removeServer = async (server: string) => {
|
||||
const draft = cloneEvent(10063, mediaServers);
|
||||
draft.tags = draft.tags.filter((t) => t[0] === "r" && !serversEqual(t[1], server));
|
||||
await publish("Remove media server", draft);
|
||||
};
|
||||
|
||||
const switchToBlossom = useAsyncErrorHandler(async () => {
|
||||
await updateSettings({ mediaUploadService: "blossom" });
|
||||
}, [updateSettings]);
|
||||
|
||||
const { register, handleSubmit, reset } = useForm({ defaultValues: { server: "" } });
|
||||
|
||||
const [confirmServer, setConfirmServer] = useState("");
|
||||
const submit = handleSubmit((values) => {
|
||||
if (mediaServers?.tags.some((t) => t[0] === "r" && serversEqual(t[1], values.server)))
|
||||
return toast({ status: "error", description: "Server already in list" });
|
||||
|
||||
setConfirmServer(new URL(values.server).toString());
|
||||
reset();
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const confirmAddServer = async () => {
|
||||
setLoading(true);
|
||||
await addServer(confirmServer);
|
||||
setConfirmServer("");
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex gap="2" direction="column" overflow="auto hidden" flex={1} px="2">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<BackButton hideFrom="lg" size="sm" />
|
||||
<Heading size="lg">Media Servers</Heading>
|
||||
{mediaServers && <DebugEventButton event={mediaServers} size="sm" ml="auto" />}
|
||||
</Flex>
|
||||
<Text fontStyle="italic" mt="-2">
|
||||
<Link href="https://github.com/hzrd149/blossom" target="_blank" color="blue.500">
|
||||
Blossom
|
||||
</Link>{" "}
|
||||
media servers are used to host your images and videos when making a post
|
||||
</Text>
|
||||
|
||||
{mediaUploadService !== "blossom" && (
|
||||
<Alert status="info">
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>Blossom not selected</AlertTitle>
|
||||
<AlertDescription>
|
||||
These servers wont be used for anything unless you set "Media upload service" to "Blossom" in the settings
|
||||
</AlertDescription>
|
||||
<br />
|
||||
<Button size="sm" variant="outline" onClick={switchToBlossom}>
|
||||
Switch to Blossom
|
||||
</Button>
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{servers.length === 0 && mediaUploadService === "blossom" && (
|
||||
<Alert
|
||||
status="error"
|
||||
variant="subtle"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
textAlign="center"
|
||||
height="xs"
|
||||
>
|
||||
<AlertIcon boxSize="40px" mr={0} />
|
||||
<AlertTitle mt={4} mb={1} fontSize="lg">
|
||||
No media servers!
|
||||
</AlertTitle>
|
||||
<AlertDescription maxWidth="sm">
|
||||
You need to add at least one media server in order to upload images and videos
|
||||
</AlertDescription>
|
||||
<Divider maxW="96" w="full" my="2" />
|
||||
<Button onClick={() => setConfirmServer("https://cdn.satellite.earth/")}>Add cdn.satellite.earth</Button>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Flex direction="column" gap="2">
|
||||
{servers.map((server) => (
|
||||
<Flex gap="2" p="2" alignItems="center" borderWidth="1px" borderRadius="lg" key={server}>
|
||||
<MediaServerFavicon server={server} size="sm" />
|
||||
<Link href={server} target="_blank" color="blue.500" fontSize="lg">
|
||||
{new URL(server).hostname}
|
||||
</Link>
|
||||
|
||||
<CloseButton ml="auto" onClick={() => removeServer(server)} />
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
<Heading size="sm" mt="2">
|
||||
Add media server
|
||||
</Heading>
|
||||
<Flex as="form" onSubmit={submit} gap="2">
|
||||
<Input {...register("server", { required: true })} required placeholder="https://cdn.satellite.earth" />
|
||||
<Button type="submit" colorScheme="primary">
|
||||
Add
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{confirmServer && (
|
||||
<Modal isOpen onClose={() => setConfirmServer("")} size="full">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Add media server</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody display="flex" p="0" flexDirection="column">
|
||||
<Box as="iframe" src={confirmServer} w="full" h="full" flex={1} />
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant="ghost" mr={3} onClick={() => setConfirmServer("")}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="primary" onClick={confirmAddServer} isLoading={loading}>
|
||||
Add Server
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MediaServersView() {
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<MediaServersPage />
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
}
|
@ -111,7 +111,7 @@ export default function DatabaseSettings() {
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton fontSize="xl">
|
||||
<DatabaseIcon mr="2" />
|
||||
<DatabaseIcon mr="2" boxSize={5} />
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Database
|
||||
</Box>
|
||||
|
@ -27,7 +27,7 @@ export default function DisplaySettings() {
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton fontSize="xl">
|
||||
<AppearanceIcon mr="2" />
|
||||
<AppearanceIcon mr="2" boxSize={5} />
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Display
|
||||
</Box>
|
||||
@ -62,9 +62,6 @@ export default function DisplaySettings() {
|
||||
</FormLabel>
|
||||
<Input id="primaryColor" type="color" maxW="120" size="sm" {...register("primaryColor")} />
|
||||
</Flex>
|
||||
<FormHelperText>
|
||||
<span>The primary color of the theme</span>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="maxPageWidth" mb="0">
|
||||
|
@ -26,7 +26,7 @@ export default function LightningSettings() {
|
||||
<>
|
||||
<h2>
|
||||
<AccordionButton fontSize="xl">
|
||||
<LightningIcon mr="2" />
|
||||
<LightningIcon mr="2" boxSize={5} />
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Lightning
|
||||
</Box>
|
||||
|
@ -25,7 +25,7 @@ export default function PerformanceSettings() {
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton fontSize="xl">
|
||||
<PerformanceIcon mr="2" />
|
||||
<PerformanceIcon mr="2" boxSize={5} />
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Performance
|
||||
</Box>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
@ -11,33 +12,45 @@ import {
|
||||
AccordionIcon,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Divider,
|
||||
Tag,
|
||||
TagLabel,
|
||||
TagCloseButton,
|
||||
useDisclosure,
|
||||
IconButton,
|
||||
Button,
|
||||
Select,
|
||||
Text,
|
||||
Link,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
Divider,
|
||||
} from "@chakra-ui/react";
|
||||
import { matchSorter } from "match-sorter";
|
||||
|
||||
import { AppSettings } from "../../services/settings/migrations";
|
||||
import { AppearanceIcon, EditIcon, NotesIcon } from "../../components/icons";
|
||||
import { EditIcon, NotesIcon } from "../../components/icons";
|
||||
import { useContextEmojis } from "../../providers/global/emoji-provider";
|
||||
import useUsersMediaServers from "../../hooks/use-user-media-servers";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
|
||||
export default function PostSettings() {
|
||||
const account = useCurrentAccount();
|
||||
const { register, setValue, getValues, watch } = useFormContext<AppSettings>();
|
||||
const emojiPicker = useDisclosure();
|
||||
const mediaServers = useUsersMediaServers(account?.pubkey);
|
||||
|
||||
const emojis = useContextEmojis();
|
||||
const [emojiSearch, setEmojiSearch] = useState("");
|
||||
|
||||
watch("quickReactions");
|
||||
watch("mediaUploadService");
|
||||
const filteredEmojis = useMemo(() => {
|
||||
const values = getValues();
|
||||
if (emojiSearch.trim()) {
|
||||
const noCustom = emojis.filter((e) => e.char && !e.url && !values.quickReactions.includes(e.char));
|
||||
return matchSorter(noCustom, emojiSearch.trim(), { keys: ["keywords"] }).slice(0, 10);
|
||||
return matchSorter(noCustom, emojiSearch.trim(), { keys: ["keywords", "char"] }).slice(0, 10);
|
||||
}
|
||||
return [];
|
||||
}, [emojiSearch, getValues().quickReactions]);
|
||||
@ -61,7 +74,7 @@ export default function PostSettings() {
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton fontSize="xl">
|
||||
<NotesIcon mr="2" />
|
||||
<NotesIcon mr="2" boxSize={5} />
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Post
|
||||
</Box>
|
||||
@ -76,7 +89,7 @@ export default function PostSettings() {
|
||||
</FormLabel>
|
||||
<Flex gap="2" wrap="wrap">
|
||||
{getValues().quickReactions.map((char, i) => (
|
||||
<Tag key={char + i}>
|
||||
<Tag key={char + i} size="lg">
|
||||
<TagLabel>{char}</TagLabel>
|
||||
{emojiPicker.isOpen && <TagCloseButton onClick={() => removeEmoji(char)} />}
|
||||
</Tag>
|
||||
@ -89,14 +102,13 @@ export default function PostSettings() {
|
||||
</Flex>
|
||||
{emojiPicker.isOpen && (
|
||||
<>
|
||||
<Divider my="2" />
|
||||
<Input
|
||||
type="search"
|
||||
w="sm"
|
||||
h="8"
|
||||
value={emojiSearch}
|
||||
onChange={(e) => setEmojiSearch(e.target.value)}
|
||||
mb="2"
|
||||
my="2"
|
||||
/>
|
||||
<Flex gap="2" wrap="wrap">
|
||||
{filteredEmojis.map((emoji) => (
|
||||
@ -115,6 +127,38 @@ export default function PostSettings() {
|
||||
</>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="theme" mb="0">
|
||||
Media upload service
|
||||
</FormLabel>
|
||||
<Select id="mediaUploadService" w="sm" {...register("mediaUploadService")}>
|
||||
<option value="nostr.build">nostr.build</option>
|
||||
<option value="blossom">Blossom</option>
|
||||
</Select>
|
||||
|
||||
{getValues().mediaUploadService === "nostr.build" && (
|
||||
<>
|
||||
<FormHelperText>
|
||||
Its a good idea to sign up and pay for an account on{" "}
|
||||
<Link href="https://nostr.build/login/" target="_blank" color="blue.500">
|
||||
nostr.build
|
||||
</Link>
|
||||
</FormHelperText>
|
||||
</>
|
||||
)}
|
||||
|
||||
{getValues().mediaUploadService === "blossom" && (!mediaServers || mediaServers.tags.length === 0) && (
|
||||
<Alert status="error" mt="2" flexWrap="wrap">
|
||||
<AlertIcon />
|
||||
<AlertTitle>Missing media servers!</AlertTitle>
|
||||
<AlertDescription>Looks like you don't have any media servers setup</AlertDescription>
|
||||
<Button as={RouterLink} colorScheme="primary" ml="auto" size="sm" to="/relays/media-servers">
|
||||
Setup servers
|
||||
</Button>
|
||||
</Alert>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="noteDifficulty" mb="0">
|
||||
Proof of work
|
||||
|
@ -49,7 +49,7 @@ export default function PrivacySettings() {
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton fontSize="xl">
|
||||
<SpyIcon mr="2" />
|
||||
<SpyIcon mr="2" boxSize={5} />
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Privacy
|
||||
</Box>
|
||||
|
@ -6,7 +6,7 @@ import dayjs from "dayjs";
|
||||
|
||||
import { Kind0ParsedContent } from "../../helpers/nostr/user-metadata";
|
||||
import { containerProps } from "./common";
|
||||
import { nostrBuildUploadImage } from "../../helpers/nostr-build";
|
||||
import { nostrBuildUploadImage } from "../../helpers/media-upload/nostr-build";
|
||||
import accountService from "../../services/account";
|
||||
import signingService from "../../services/signing";
|
||||
import { COMMON_CONTACT_RELAY } from "../../const";
|
||||
|
@ -9,7 +9,7 @@ import { createEmojiTags, ensureNotifyContentMentions } from "../../../../helper
|
||||
import { useContextEmojis } from "../../../../providers/global/emoji-provider";
|
||||
import { MagicInput, RefType } from "../../../../components/magic-textarea";
|
||||
import StreamZapButton from "../../components/stream-zap-button";
|
||||
import { nostrBuildUploadImage } from "../../../../helpers/nostr-build";
|
||||
import { nostrBuildUploadImage } from "../../../../helpers/media-upload/nostr-build";
|
||||
import { useUserInbox } from "../../../../hooks/use-user-mailboxes";
|
||||
import { usePublishEvent } from "../../../../providers/global/publish-provider";
|
||||
import { useReadRelays } from "../../../../hooks/use-client-relays";
|
||||
|
@ -20,7 +20,7 @@ import { useSigningContext } from "../../../providers/global/signing-provider";
|
||||
import MagicTextArea, { RefType } from "../../../components/magic-textarea";
|
||||
import { useContextEmojis } from "../../../providers/global/emoji-provider";
|
||||
import { TrustProvider } from "../../../providers/local/trust";
|
||||
import { nostrBuildUploadImage } from "../../../helpers/nostr-build";
|
||||
import { nostrBuildUploadImage } from "../../../helpers/media-upload/nostr-build";
|
||||
import { UploadImageIcon } from "../../../components/icons";
|
||||
import { unique } from "../../../helpers/array";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
|
68
yarn.lock
68
yarn.lock
@ -2495,6 +2495,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.2.0.tgz#a12cda60f3cf1ab5d7c77068c3711d2366649ed7"
|
||||
integrity sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==
|
||||
|
||||
"@noble/ciphers@^0.5.1":
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.5.1.tgz#292f388b69c9ed80d49dca1a5cbfd4ff06852111"
|
||||
integrity sha512-aNE06lbe36ifvMbbWvmmF/8jx6EQPu2HVg70V95T+iGjOuYwPpAccwAQc2HlXO2D0aiQ3zavbMga4jjWnrpiPA==
|
||||
|
||||
"@noble/curves@1.1.0", "@noble/curves@~1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d"
|
||||
@ -2531,6 +2536,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699"
|
||||
integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==
|
||||
|
||||
"@noble/hashes@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426"
|
||||
integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==
|
||||
|
||||
"@noble/secp256k1@^1.7.0":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c"
|
||||
@ -2702,6 +2712,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
|
||||
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
|
||||
|
||||
"@scure/base@^1.1.6":
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d"
|
||||
integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==
|
||||
|
||||
"@scure/base@~1.1.0", "@scure/base@~1.1.4":
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157"
|
||||
@ -3293,6 +3308,26 @@ better-path-resolve@1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/bezier-js/-/bezier-js-6.1.4.tgz#c7828f6c8900562b69d5040afb881bcbdad82001"
|
||||
integrity sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==
|
||||
|
||||
blossom-client@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/blossom-client/-/blossom-client-0.4.0.tgz#1607e5f862aa14a4f7e3ca91c025c778477b0163"
|
||||
integrity sha512-kMEMmvaH2sJ2vyJoaG28raBmTtohbFgAJ/wu0/wvNCQORxsfCrmDEmF0QuKCaFYEL4eUoBOTyhEPzhd7dekjlA==
|
||||
dependencies:
|
||||
cross-fetch "^4.0.0"
|
||||
|
||||
blossom-drive-client@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/blossom-drive-client/-/blossom-drive-client-0.1.0.tgz#9cb2b20df55ad11789f6492086110633e94bdce5"
|
||||
integrity sha512-E9KaOeqJUbhdexL9woYkViz72mKaNxNBaLiAVq2+D1N1F0d4NZ+zytNevr/+pL/rSQhbMOMZLb3Wap3UKKj/0w==
|
||||
dependencies:
|
||||
"@noble/hashes" "^1.4.0"
|
||||
"@scure/base" "^1.1.6"
|
||||
blossom-client "^0.4.0"
|
||||
events "^3.3.0"
|
||||
mime "^4.0.1"
|
||||
nanoid "^5.0.6"
|
||||
nostr-tools "^2.3.2"
|
||||
|
||||
blurhash@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-2.0.5.tgz#efde729fc14a2f03571a6aa91b49cba80d1abe4b"
|
||||
@ -3634,6 +3669,13 @@ crelt@^1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
|
||||
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
|
||||
|
||||
cross-fetch@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983"
|
||||
integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==
|
||||
dependencies:
|
||||
node-fetch "^2.6.12"
|
||||
|
||||
cross-spawn@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
|
||||
@ -5199,6 +5241,11 @@ micromatch@^4.0.2, micromatch@^4.0.4:
|
||||
braces "^3.0.2"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
mime@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-4.0.1.tgz#ad7563d1bfe30253ad97dedfae2b1009d01b9470"
|
||||
integrity sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==
|
||||
|
||||
min-indent@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
|
||||
@ -5266,6 +5313,11 @@ nanoid@^5.0.4:
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.5.tgz#5112efb5c0caf4fc80680d66d303c65233a79fdd"
|
||||
integrity sha512-/Veqm+QKsyMY3kqi4faWplnY1u+VuKO3dD2binyPIybP31DRO29bPF+1mszgLnrR2KqSLceFLBNw0zmvDzN1QQ==
|
||||
|
||||
nanoid@^5.0.6:
|
||||
version "5.0.6"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.6.tgz#7f99a033aa843e4dcf9778bdaec5eb02f4dc44d5"
|
||||
integrity sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==
|
||||
|
||||
nearley@^2.20.1:
|
||||
version "2.20.1"
|
||||
resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474"
|
||||
@ -5312,7 +5364,7 @@ ngraph.random@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ngraph.random/-/ngraph.random-1.1.0.tgz#5345c4bb63865c85d98ee6f13eab1395d8545a90"
|
||||
integrity sha512-h25UdUN/g8U7y29TzQtRm/GvGr70lK37yQPvPKXXuVfs7gCm82WipYFZcksQfeKumtOemAzBIcT7lzzyK/edLw==
|
||||
|
||||
node-fetch@^2.5.0:
|
||||
node-fetch@^2.5.0, node-fetch@^2.6.12:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
@ -5369,6 +5421,20 @@ nostr-tools@^2.1.3:
|
||||
optionalDependencies:
|
||||
nostr-wasm v0.1.0
|
||||
|
||||
nostr-tools@^2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.3.2.tgz#74e92898b000413661b091c5829f762cb4420e66"
|
||||
integrity sha512-8ceZ2ItkAGjR5b9+QOkkV9KWBOK0WPlpFrPPXmbWnNMcnlj9zB7rjdYPK2sV/OK4Ty9J3xL6+bvYKY77gup5EQ==
|
||||
dependencies:
|
||||
"@noble/ciphers" "^0.5.1"
|
||||
"@noble/curves" "1.2.0"
|
||||
"@noble/hashes" "1.3.1"
|
||||
"@scure/base" "1.1.1"
|
||||
"@scure/bip32" "1.3.1"
|
||||
"@scure/bip39" "1.2.1"
|
||||
optionalDependencies:
|
||||
nostr-wasm v0.1.0
|
||||
|
||||
nostr-wasm@v0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94"
|
||||
|
Loading…
x
Reference in New Issue
Block a user