From 8d46272558a31d1c2fc5f64d6d08f981ac8e0ca4 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Wed, 20 Mar 2024 15:28:40 -0500 Subject: [PATCH] add blossom media upload option --- .changeset/modern-fishes-own.md | 5 + package.json | 2 + src/app.tsx | 2 + .../debug-modal/event-debug-modal.tsx | 2 +- src/components/icons.tsx | 1 + .../media-server/media-server-favicon.tsx | 18 ++ src/components/post-modal/index.tsx | 2 +- src/helpers/media-upload/blossom.ts | 23 +++ src/helpers/{ => media-upload}/nostr-build.ts | 2 +- src/hooks/use-textarea-upload-file.ts | 70 ++++--- src/hooks/use-user-media-servers.ts | 10 + src/services/settings/migrations.ts | 7 +- .../components/community-create-modal.tsx | 2 +- src/views/relays/contact-list/index.tsx | 17 +- src/views/relays/index.tsx | 30 ++- src/views/relays/mailboxes/index.tsx | 25 +-- src/views/relays/media-servers/index.tsx | 192 ++++++++++++++++++ src/views/settings/database-settings.tsx | 2 +- src/views/settings/display-settings.tsx | 5 +- src/views/settings/lightning-settings.tsx | 2 +- src/views/settings/performance-settings.tsx | 2 +- src/views/settings/post-settings.tsx | 58 +++++- src/views/settings/privacy-settings.tsx | 2 +- src/views/signup/create-step.tsx | 2 +- .../stream/stream-chat/stream-chat-form.tsx | 2 +- src/views/thread/components/reply-form.tsx | 2 +- yarn.lock | 68 ++++++- 27 files changed, 460 insertions(+), 95 deletions(-) create mode 100644 .changeset/modern-fishes-own.md create mode 100644 src/components/media-server/media-server-favicon.tsx create mode 100644 src/helpers/media-upload/blossom.ts rename src/helpers/{ => media-upload}/nostr-build.ts (95%) create mode 100644 src/hooks/use-user-media-servers.ts create mode 100644 src/views/relays/media-servers/index.tsx diff --git a/.changeset/modern-fishes-own.md b/.changeset/modern-fishes-own.md new file mode 100644 index 000000000..0cf4ee084 --- /dev/null +++ b/.changeset/modern-fishes-own.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add blossom media upload option diff --git a/package.json b/package.json index 321915195..32cd3ba60 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app.tsx b/src/app.tsx index b8decf155..0236f2ee4 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -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: }, { path: "cache", element: }, { path: "mailboxes", element: }, + { path: "media-servers", element: }, { path: "nip05", element: }, { path: "contacts", element: }, { path: "sets", element: }, diff --git a/src/components/debug-modal/event-debug-modal.tsx b/src/components/debug-modal/event-debug-modal.tsx index e69ac58e2..fbd1ba794 100644 --- a/src/components/debug-modal/event-debug-modal.tsx +++ b/src/components/debug-modal/event-debug-modal.tsx @@ -84,7 +84,7 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent {event.id} - +
diff --git a/src/components/icons.tsx b/src/components/icons.tsx index bdefafda7..90abc6bd6 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -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; diff --git a/src/components/media-server/media-server-favicon.tsx b/src/components/media-server/media-server-favicon.tsx new file mode 100644 index 000000000..4129ca863 --- /dev/null +++ b/src/components/media-server/media-server-favicon.tsx @@ -0,0 +1,18 @@ +import { useMemo } from "react"; +import { Avatar, AvatarProps } from "@chakra-ui/react"; + +import { MediaServerIcon } from "../icons"; + +export type RelayFaviconProps = Omit & { + 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 } overflow="hidden" {...props} />; +} diff --git a/src/components/post-modal/index.tsx b/src/components/post-modal/index.tsx index 5d61ed55a..d297c4438 100644 --- a/src/components/post-modal/index.tsx +++ b/src/components/post-modal/index.tsx @@ -215,7 +215,7 @@ export default function PostModal({ onChange={onFileInputChange} /> } + icon={} aria-label="Upload Image" title="Upload Image" onClick={() => imageUploadRef.current?.click()} diff --git a/src/helpers/media-upload/blossom.ts b/src/helpers/media-upload/blossom.ts new file mode 100644 index 000000000..9bd6837d5 --- /dev/null +++ b/src/helpers/media-upload/blossom.ts @@ -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]; +} diff --git a/src/helpers/nostr-build.ts b/src/helpers/media-upload/nostr-build.ts similarity index 95% rename from src/helpers/nostr-build.ts rename to src/helpers/media-upload/nostr-build.ts index bfb777506..b15e6c05f 100644 --- a/src/helpers/nostr-build.ts +++ b/src/helpers/media-upload/nostr-build.ts @@ -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"; diff --git a/src/hooks/use-textarea-upload-file.ts b/src/hooks/use-textarea-upload-file.ts index 7f6ad89a6..0b0ddbd75 100644 --- a/src/hooks/use-textarea-upload-file.ts +++ b/src/hooks/use-textarea-upload-file.ts @@ -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, @@ -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>( diff --git a/src/hooks/use-user-media-servers.ts b/src/hooks/use-user-media-servers.ts new file mode 100644 index 000000000..a2ee7bffe --- /dev/null +++ b/src/hooks/use-user-media-servers.ts @@ -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; +} diff --git a/src/services/settings/migrations.ts b/src/services/settings/migrations.ts index 46e3bc612..61cdcaf37 100644 --- a/src/services/settings/migrations.ts +++ b/src/services/settings/migrations.ts @@ -34,8 +34,12 @@ export type AppSettingsV4 = Omit & { version: 4; loadO export type AppSettingsV5 = Omit & { version: 5; hideUsernames: boolean }; export type AppSettingsV6 = Omit & { version: 6; noteDifficulty: number | null }; export type AppSettingsV7 = Omit & { version: 7; autoDecryptDMs: boolean }; +export type AppSettingsV8 = Omit & { + 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", diff --git a/src/views/communities/components/community-create-modal.tsx b/src/views/communities/components/community-create-modal.tsx index 7493692d7..e0a686a23 100644 --- a/src/views/communities/components/community-create-modal.tsx +++ b/src/views/communities/components/community-create-modal.tsx @@ -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"; diff --git a/src/views/relays/contact-list/index.tsx b/src/views/relays/contact-list/index.tsx index 9a52c749f..113dce127 100644 --- a/src/views/relays/contact-list/index.tsx +++ b/src/views/relays/contact-list/index.tsx @@ -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 ( - - - - {url} - - - ); -} - export default function ContactListRelaysView() { const account = useCurrentAccount(); const contacts = useUserContactList(account?.pubkey); diff --git a/src/views/relays/index.tsx b/src/views/relays/index.tsx index aa9e168ee..434f8dc2b 100644 --- a/src/views/relays/index.tsx +++ b/src/views/relays/index.tsx @@ -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 {account && ( - + <> + + + )} {nip05 && ( + + + )} + + {servers.length === 0 && mediaUploadService === "blossom" && ( + + + + No media servers! + + + You need to add at least one media server in order to upload images and videos + + + + + )} + + + {servers.map((server) => ( + + + + {new URL(server).hostname} + + + removeServer(server)} /> + + ))} + + + + Add media server + + + + + + + {confirmServer && ( + setConfirmServer("")} size="full"> + + + Add media server + + + + + + + + + + + + )} + + ); +} + +export default function MediaServersView() { + return ( + + + + ); +} diff --git a/src/views/settings/database-settings.tsx b/src/views/settings/database-settings.tsx index 20c52e588..fd7968c98 100644 --- a/src/views/settings/database-settings.tsx +++ b/src/views/settings/database-settings.tsx @@ -111,7 +111,7 @@ export default function DatabaseSettings() {

- + Database diff --git a/src/views/settings/display-settings.tsx b/src/views/settings/display-settings.tsx index 969ecac0d..30329e166 100644 --- a/src/views/settings/display-settings.tsx +++ b/src/views/settings/display-settings.tsx @@ -27,7 +27,7 @@ export default function DisplaySettings() {

- + Display @@ -62,9 +62,6 @@ export default function DisplaySettings() { - - The primary color of the theme - diff --git a/src/views/settings/lightning-settings.tsx b/src/views/settings/lightning-settings.tsx index 2beca758b..6350f8840 100644 --- a/src/views/settings/lightning-settings.tsx +++ b/src/views/settings/lightning-settings.tsx @@ -26,7 +26,7 @@ export default function LightningSettings() { <>

- + Lightning diff --git a/src/views/settings/performance-settings.tsx b/src/views/settings/performance-settings.tsx index a7c6214ba..3e1681eb9 100644 --- a/src/views/settings/performance-settings.tsx +++ b/src/views/settings/performance-settings.tsx @@ -25,7 +25,7 @@ export default function PerformanceSettings() {

- + Performance diff --git a/src/views/settings/post-settings.tsx b/src/views/settings/post-settings.tsx index 159958435..7ace61961 100644 --- a/src/views/settings/post-settings.tsx +++ b/src/views/settings/post-settings.tsx @@ -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(); 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() {

- + Post @@ -76,7 +89,7 @@ export default function PostSettings() { {getValues().quickReactions.map((char, i) => ( - + {char} {emojiPicker.isOpen && removeEmoji(char)} />} @@ -89,14 +102,13 @@ export default function PostSettings() { {emojiPicker.isOpen && ( <> - setEmojiSearch(e.target.value)} - mb="2" + my="2" /> {filteredEmojis.map((emoji) => ( @@ -115,6 +127,38 @@ export default function PostSettings() { )} + + + Media upload service + + + + {getValues().mediaUploadService === "nostr.build" && ( + <> + + Its a good idea to sign up and pay for an account on{" "} + + nostr.build + + + + )} + + {getValues().mediaUploadService === "blossom" && (!mediaServers || mediaServers.tags.length === 0) && ( + + + Missing media servers! + Looks like you don't have any media servers setup + + + )} + + Proof of work diff --git a/src/views/settings/privacy-settings.tsx b/src/views/settings/privacy-settings.tsx index f5ff0dc8f..d5ccb169a 100644 --- a/src/views/settings/privacy-settings.tsx +++ b/src/views/settings/privacy-settings.tsx @@ -49,7 +49,7 @@ export default function PrivacySettings() {

- + Privacy diff --git a/src/views/signup/create-step.tsx b/src/views/signup/create-step.tsx index 9cd6f2f53..138912642 100644 --- a/src/views/signup/create-step.tsx +++ b/src/views/signup/create-step.tsx @@ -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"; diff --git a/src/views/streams/stream/stream-chat/stream-chat-form.tsx b/src/views/streams/stream/stream-chat/stream-chat-form.tsx index 3b195c21d..8496809bd 100644 --- a/src/views/streams/stream/stream-chat/stream-chat-form.tsx +++ b/src/views/streams/stream/stream-chat/stream-chat-form.tsx @@ -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"; diff --git a/src/views/thread/components/reply-form.tsx b/src/views/thread/components/reply-form.tsx index 392ca4275..38e9f0865 100644 --- a/src/views/thread/components/reply-form.tsx +++ b/src/views/thread/components/reply-form.tsx @@ -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"; diff --git a/yarn.lock b/yarn.lock index 477e7085c..5e5a1a71d 100644 --- a/yarn.lock +++ b/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"