add blossom media upload option

This commit is contained in:
hzrd149 2024-03-20 15:28:40 -05:00
parent ed1dc8ded6
commit 8d46272558
27 changed files with 460 additions and 95 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add blossom media upload option

View File

@ -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",

View File

@ -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 /> },

View File

@ -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)} />

View File

@ -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;

View 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} />;
}

View File

@ -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()}

View 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];
}

View File

@ -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";

View File

@ -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>>(

View 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;
}

View File

@ -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",

View File

@ -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";

View File

@ -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);

View File

@ -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

View File

@ -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{" "}

View 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>
);
}

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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"