mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-11 21:29:26 +02:00
blur images in stream chat
This commit is contained in:
parent
5a537ab9ab
commit
e4b40dda68
5
.changeset/brave-dolls-glow.md
Normal file
5
.changeset/brave-dolls-glow.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Blur images in stream chat
|
@ -2,7 +2,7 @@ import { Box, Image, ImageProps, Link, useDisclosure } from "@chakra-ui/react";
|
|||||||
import appSettings from "../../services/app-settings";
|
import appSettings from "../../services/app-settings";
|
||||||
import { ImageGalleryLink } from "../image-gallery";
|
import { ImageGalleryLink } from "../image-gallery";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||||
import { useTrusted } from "../note/trust";
|
import { useTrusted } from "../../providers/trust";
|
||||||
import OpenGraphCard from "../open-graph-card";
|
import OpenGraphCard from "../open-graph-card";
|
||||||
|
|
||||||
const BlurredImage = (props: ImageProps) => {
|
const BlurredImage = (props: ImageProps) => {
|
||||||
|
@ -11,10 +11,9 @@ import {
|
|||||||
useDisclosure,
|
useDisclosure,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { ExternalLinkIcon, LightningIcon, QrCodeIcon } from "./icons";
|
import { ExternalLinkIcon, QrCodeIcon } from "./icons";
|
||||||
import QrCodeSvg from "./qr-code-svg";
|
import QrCodeSvg from "./qr-code-svg";
|
||||||
import { CopyIconButton } from "./copy-icon-button";
|
import { CopyIconButton } from "./copy-icon-button";
|
||||||
import { useIsMobile } from "../hooks/use-is-mobile";
|
|
||||||
|
|
||||||
export default function InvoiceModal({
|
export default function InvoiceModal({
|
||||||
invoice,
|
invoice,
|
||||||
@ -22,17 +21,20 @@ export default function InvoiceModal({
|
|||||||
onPaid,
|
onPaid,
|
||||||
...props
|
...props
|
||||||
}: Omit<ModalProps, "children"> & { invoice: string; onPaid: () => void }) {
|
}: Omit<ModalProps, "children"> & { invoice: string; onPaid: () => void }) {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const showQr = useDisclosure();
|
const showQr = useDisclosure();
|
||||||
|
|
||||||
const payWithWebLn = async (invoice: string) => {
|
const payWithWebLn = async (invoice: string) => {
|
||||||
if (window.webln && invoice) {
|
try {
|
||||||
if (!window.webln.enabled) await window.webln.enable();
|
if (window.webln && invoice) {
|
||||||
await window.webln.sendPayment(invoice);
|
if (!window.webln.enabled) await window.webln.enable();
|
||||||
|
await window.webln.sendPayment(invoice);
|
||||||
|
|
||||||
if (onPaid) onPaid();
|
if (onPaid) onPaid();
|
||||||
onClose();
|
onClose();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const payWithApp = async (invoice: string) => {
|
const payWithApp = async (invoice: string) => {
|
||||||
|
@ -37,9 +37,7 @@ export function RepostButton({ event }: { event: NostrEvent }) {
|
|||||||
await nostrPostAction(clientRelaysService.getWriteUrls(), repost);
|
await nostrPostAction(clientRelaysService.getWriteUrls(), repost);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
toast({ status: "error", description: e.message });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
@ -9,7 +9,7 @@ import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
|
|||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import appSettings from "../../services/app-settings";
|
import appSettings from "../../services/app-settings";
|
||||||
import EventVerificationIcon from "../event-verification-icon";
|
import EventVerificationIcon from "../event-verification-icon";
|
||||||
import { TrustProvider } from "./trust";
|
import { TrustProvider } from "../../providers/trust";
|
||||||
import { NoteLink } from "../note-link";
|
import { NoteLink } from "../note-link";
|
||||||
|
|
||||||
export default function EmbeddedNote({ note }: { note: NostrEvent }) {
|
export default function EmbeddedNote({ note }: { note: NostrEvent }) {
|
||||||
|
@ -32,7 +32,7 @@ import { RepostButton } from "./buttons/repost-button";
|
|||||||
import { QuoteRepostButton } from "./buttons/quote-repost-button";
|
import { QuoteRepostButton } from "./buttons/quote-repost-button";
|
||||||
import { ExternalLinkIcon } from "../icons";
|
import { ExternalLinkIcon } from "../icons";
|
||||||
import NoteContentWithWarning from "./note-content-with-warning";
|
import NoteContentWithWarning from "./note-content-with-warning";
|
||||||
import { TrustProvider } from "./trust";
|
import { TrustProvider } from "../../providers/trust";
|
||||||
import { NoteLink } from "../note-link";
|
import { NoteLink } from "../note-link";
|
||||||
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||||
|
|
||||||
|
@ -21,11 +21,10 @@ import {
|
|||||||
renderOpenGraphUrl,
|
renderOpenGraphUrl,
|
||||||
} from "../embed-types";
|
} from "../embed-types";
|
||||||
import { ImageGalleryProvider } from "../image-gallery";
|
import { ImageGalleryProvider } from "../image-gallery";
|
||||||
import { useTrusted } from "./trust";
|
|
||||||
import { renderRedditUrl } from "../embed-types/reddit";
|
import { renderRedditUrl } from "../embed-types/reddit";
|
||||||
import EmbeddedContent from "../embeded-content";
|
import EmbeddedContent from "../embeded-content";
|
||||||
|
|
||||||
function buildContents(event: NostrEvent | DraftNostrEvent, trusted = false) {
|
function buildContents(event: NostrEvent | DraftNostrEvent) {
|
||||||
let content: EmbedableContent = [event.content.trim()];
|
let content: EmbedableContent = [event.content.trim()];
|
||||||
|
|
||||||
// common
|
// common
|
||||||
@ -70,8 +69,7 @@ export type NoteContentsProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NoteContents = React.memo(({ event, maxHeight }: NoteContentsProps) => {
|
export const NoteContents = React.memo(({ event, maxHeight }: NoteContentsProps) => {
|
||||||
const trusted = useTrusted();
|
const content = buildContents(event);
|
||||||
const content = buildContents(event, trusted);
|
|
||||||
const expand = useExpand();
|
const expand = useExpand();
|
||||||
const [innerHeight, setInnerHeight] = useState(0);
|
const [innerHeight, setInnerHeight] = useState(0);
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
@ -53,12 +53,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
|
|||||||
await results.onComplete;
|
await results.onComplete;
|
||||||
deleteModal.onClose();
|
deleteModal.onClose();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
toast({
|
|
||||||
status: "error",
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import { ImageIcon } from "../icons";
|
|||||||
import { NoteLink } from "../note-link";
|
import { NoteLink } from "../note-link";
|
||||||
import { NoteContents } from "../note/note-contents";
|
import { NoteContents } from "../note/note-contents";
|
||||||
import { PostResults } from "./post-results";
|
import { PostResults } from "./post-results";
|
||||||
import { TrustProvider } from "../note/trust";
|
import { TrustProvider } from "../../providers/trust";
|
||||||
|
|
||||||
function emptyDraft(): DraftNostrEvent {
|
function emptyDraft(): DraftNostrEvent {
|
||||||
return {
|
return {
|
||||||
@ -96,12 +96,7 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
|||||||
setDraft((d) => ({ ...d, content: (d.content += imageUrl) }));
|
setDraft((d) => ({ ...d, content: (d.content += imageUrl) }));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
toast({
|
|
||||||
status: "error",
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
};
|
};
|
||||||
|
@ -9,7 +9,7 @@ import { NoteMenu } from "./note/note-menu";
|
|||||||
import { UserAvatar } from "./user-avatar";
|
import { UserAvatar } from "./user-avatar";
|
||||||
import { UserDnsIdentityIcon } from "./user-dns-identity-icon";
|
import { UserDnsIdentityIcon } from "./user-dns-identity-icon";
|
||||||
import { UserLink } from "./user-link";
|
import { UserLink } from "./user-link";
|
||||||
import { TrustProvider } from "./note/trust";
|
import { TrustProvider } from "../providers/trust";
|
||||||
import { safeJson } from "../helpers/parse";
|
import { safeJson } from "../helpers/parse";
|
||||||
import { verifySignature } from "nostr-tools";
|
import { verifySignature } from "nostr-tools";
|
||||||
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
||||||
|
@ -59,13 +59,13 @@ export default function ZapModal({
|
|||||||
initialAmount,
|
initialAmount,
|
||||||
...props
|
...props
|
||||||
}: ZapModalProps) {
|
}: ZapModalProps) {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
const metadata = useUserMetadata(pubkey);
|
const metadata = useUserMetadata(pubkey);
|
||||||
const { requestSignature } = useSigningContext();
|
const { requestSignature } = useSigningContext();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [promptInvoice, setPromptInvoice] = useState<string>();
|
const [promptInvoice, setPromptInvoice] = useState<string>();
|
||||||
const { isOpen: showQr, onToggle: toggleQr } = useDisclosure();
|
const { isOpen: showQr, onToggle: toggleQr } = useDisclosure();
|
||||||
const isMobile = useIsMobile();
|
const { customZapAmounts } = useSubject(appSettings);
|
||||||
const { zapAmounts } = useSubject(appSettings);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@ -76,7 +76,7 @@ export default function ZapModal({
|
|||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
amount: initialAmount ?? zapAmounts[0],
|
amount: initialAmount ?? (parseInt(customZapAmounts.split(",")[0]) || 100),
|
||||||
comment: initialComment ?? "",
|
comment: initialComment ?? "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -160,19 +160,11 @@ export default function ZapModal({
|
|||||||
}, 1000 * 2);
|
}, 1000 * 2);
|
||||||
};
|
};
|
||||||
|
|
||||||
const payInvoice = (invoice: string) => {
|
const payInvoice = async (invoice: string) => {
|
||||||
switch (appSettings.value.lightningPayMode) {
|
if (appSettings.value.autoPayWithWebLN) {
|
||||||
case "webln":
|
await payWithWebLn(invoice);
|
||||||
payWithWebLn(invoice);
|
|
||||||
break;
|
|
||||||
case "external":
|
|
||||||
payWithApp(invoice);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
case "prompt":
|
|
||||||
setPromptInvoice(invoice);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
setPromptInvoice(invoice);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@ -228,11 +220,14 @@ export default function ZapModal({
|
|||||||
<UserLink pubkey={pubkey} />
|
<UserLink pubkey={pubkey} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex gap="2" alignItems="center" flexWrap="wrap">
|
<Flex gap="2" alignItems="center" flexWrap="wrap">
|
||||||
{zapAmounts.map((amount, i) => (
|
{customZapAmounts
|
||||||
<Button key={amount + i} onClick={() => setValue("amount", amount)} size="sm" variant="outline">
|
.split(",")
|
||||||
{amount}
|
.map((v) => parseInt(v))
|
||||||
</Button>
|
.map((amount, i) => (
|
||||||
))}
|
<Button key={amount + i} onClick={() => setValue("amount", amount)} size="sm" variant="outline">
|
||||||
|
{amount}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<InputGroup maxWidth={32}>
|
<InputGroup maxWidth={32}>
|
||||||
|
@ -13,12 +13,7 @@ export default function useAppSettings() {
|
|||||||
try {
|
try {
|
||||||
return replaceSettings({ ...settings, ...newSettings });
|
return replaceSettings({ ...settings, ...newSettings });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
toast({
|
|
||||||
status: "error",
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[settings]
|
[settings]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback, useContext, useState } from "react";
|
import React, { useCallback, useContext, useState } from "react";
|
||||||
import InvoiceModal from "../components/invoice-modal";
|
import InvoiceModal from "../components/invoice-modal";
|
||||||
import createDefer, { Deferred } from "../classes/deferred";
|
import createDefer, { Deferred } from "../classes/deferred";
|
||||||
|
import appSettings from "../services/app-settings";
|
||||||
|
|
||||||
export type InvoiceModalContext = {
|
export type InvoiceModalContext = {
|
||||||
requestPay: (invoice: string) => Promise<void>;
|
requestPay: (invoice: string) => Promise<void>;
|
||||||
@ -20,7 +21,17 @@ export const InvoiceModalProvider = ({ children }: { children: React.ReactNode }
|
|||||||
const [invoice, setInvoice] = useState<string>();
|
const [invoice, setInvoice] = useState<string>();
|
||||||
const [defer, setDefer] = useState<Deferred<void>>();
|
const [defer, setDefer] = useState<Deferred<void>>();
|
||||||
|
|
||||||
const requestPay = useCallback((invoice: string) => {
|
const requestPay = useCallback(async (invoice: string) => {
|
||||||
|
if (window.webln && appSettings.value.autoPayWithWebLN) {
|
||||||
|
try {
|
||||||
|
if (!window.webln.enabled) await window.webln.enable();
|
||||||
|
await window.webln.sendPayment(invoice);
|
||||||
|
|
||||||
|
handlePaid();
|
||||||
|
return;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
const defer = createDefer<void>();
|
const defer = createDefer<void>();
|
||||||
setDefer(defer);
|
setDefer(defer);
|
||||||
setInvoice(invoice);
|
setInvoice(invoice);
|
||||||
|
@ -37,12 +37,7 @@ export const SigningProvider = ({ children }: { children: React.ReactNode }) =>
|
|||||||
if (!current) throw new Error("No account");
|
if (!current) throw new Error("No account");
|
||||||
return await signingService.requestSignature(draft, current);
|
return await signingService.requestSignature(draft, current);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
toast({
|
|
||||||
status: "error",
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[toast, current]
|
[toast, current]
|
||||||
@ -53,12 +48,7 @@ export const SigningProvider = ({ children }: { children: React.ReactNode }) =>
|
|||||||
if (!current) throw new Error("No account");
|
if (!current) throw new Error("No account");
|
||||||
return await signingService.requestDecrypt(data, pubkey, current);
|
return await signingService.requestDecrypt(data, pubkey, current);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
toast({
|
|
||||||
status: "error",
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[toast, current]
|
[toast, current]
|
||||||
@ -69,12 +59,7 @@ export const SigningProvider = ({ children }: { children: React.ReactNode }) =>
|
|||||||
if (!current) throw new Error("No account");
|
if (!current) throw new Error("No account");
|
||||||
return await signingService.requestEncrypt(data, pubkey, current);
|
return await signingService.requestEncrypt(data, pubkey, current);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
toast({
|
|
||||||
status: "error",
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[toast, current]
|
[toast, current]
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { PropsWithChildren, useContext } from "react";
|
import React, { PropsWithChildren, useContext } from "react";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||||
import clientFollowingService from "../../services/client-following";
|
import clientFollowingService from "../services/client-following";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../hooks/use-subject";
|
||||||
|
|
||||||
const TrustContext = React.createContext<boolean>(false);
|
const TrustContext = React.createContext<boolean>(false);
|
||||||
|
|
@ -9,12 +9,6 @@ import db from "./db";
|
|||||||
|
|
||||||
const DTAG = "nostrudel-settings";
|
const DTAG = "nostrudel-settings";
|
||||||
|
|
||||||
export enum LightningPayMode {
|
|
||||||
Prompt = "prompt",
|
|
||||||
Webln = "webln",
|
|
||||||
External = "external",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AppSettings = {
|
export type AppSettings = {
|
||||||
colorMode: ColorMode;
|
colorMode: ColorMode;
|
||||||
blurImages: boolean;
|
blurImages: boolean;
|
||||||
@ -22,8 +16,10 @@ export type AppSettings = {
|
|||||||
proxyUserMedia: boolean;
|
proxyUserMedia: boolean;
|
||||||
showReactions: boolean;
|
showReactions: boolean;
|
||||||
showSignatureVerification: boolean;
|
showSignatureVerification: boolean;
|
||||||
lightningPayMode: LightningPayMode;
|
|
||||||
zapAmounts: number[];
|
autoPayWithWebLN: boolean;
|
||||||
|
customZapAmounts: string;
|
||||||
|
|
||||||
primaryColor: string;
|
primaryColor: string;
|
||||||
imageProxy: string;
|
imageProxy: string;
|
||||||
corsProxy: string;
|
corsProxy: string;
|
||||||
@ -40,8 +36,10 @@ export const defaultSettings: AppSettings = {
|
|||||||
proxyUserMedia: false,
|
proxyUserMedia: false,
|
||||||
showReactions: true,
|
showReactions: true,
|
||||||
showSignatureVerification: false,
|
showSignatureVerification: false,
|
||||||
lightningPayMode: LightningPayMode.Prompt,
|
|
||||||
zapAmounts: [50, 200, 500, 1000],
|
autoPayWithWebLN: true,
|
||||||
|
customZapAmounts: "50,200,500,1000,2000,5000",
|
||||||
|
|
||||||
primaryColor: "#8DB600",
|
primaryColor: "#8DB600",
|
||||||
imageProxy: "",
|
imageProxy: "",
|
||||||
corsProxy: "",
|
corsProxy: "",
|
||||||
|
@ -4,7 +4,7 @@ import { ArrowDownSIcon, ArrowUpSIcon } from "../../components/icons";
|
|||||||
import { Note } from "../../components/note";
|
import { Note } from "../../components/note";
|
||||||
import { countReplies, ThreadItem as ThreadItemData } from "../../helpers/thread";
|
import { countReplies, ThreadItem as ThreadItemData } from "../../helpers/thread";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||||
import { TrustProvider } from "../../components/note/trust";
|
import { TrustProvider } from "../../providers/trust";
|
||||||
|
|
||||||
export type ThreadItemProps = {
|
export type ThreadItemProps = {
|
||||||
post: ThreadItemData;
|
post: ThreadItemData;
|
||||||
|
@ -243,12 +243,7 @@ export const ProfileEditView = () => {
|
|||||||
|
|
||||||
await results.onComplete;
|
await results.onComplete;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
toast({
|
|
||||||
status: "error",
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -61,9 +61,7 @@ function RelaysPage() {
|
|||||||
}
|
}
|
||||||
setRelayInputValue("");
|
setRelayInputValue("");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
toast({ status: "error", description: e.message });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const savePending = async () => {
|
const savePending = async () => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button, Flex, Accordion, Link } from "@chakra-ui/react";
|
import { Button, Flex, Accordion, Link, useToast } from "@chakra-ui/react";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { GithubIcon, ToolsIcon } from "../../components/icons";
|
import { GithubIcon, ToolsIcon } from "../../components/icons";
|
||||||
import LightningSettings from "./lightning-settings";
|
import LightningSettings from "./lightning-settings";
|
||||||
@ -10,6 +10,7 @@ import useAppSettings from "../../hooks/use-app-settings";
|
|||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
|
||||||
export default function SettingsView() {
|
export default function SettingsView() {
|
||||||
|
const toast = useToast();
|
||||||
const { updateSettings, ...settings } = useAppSettings();
|
const { updateSettings, ...settings } = useAppSettings();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@ -18,7 +19,12 @@ export default function SettingsView() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const saveSettings = form.handleSubmit(async (values) => {
|
const saveSettings = form.handleSubmit(async (values) => {
|
||||||
await updateSettings(values);
|
try {
|
||||||
|
await updateSettings(values);
|
||||||
|
toast({ title: "Settings saved", status: "success" });
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -10,13 +10,15 @@ import {
|
|||||||
FormHelperText,
|
FormHelperText,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
|
Switch,
|
||||||
|
FormErrorMessage,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { LightningIcon } from "../../components/icons";
|
import { LightningIcon } from "../../components/icons";
|
||||||
import { AppSettings } from "../../services/user-app-settings";
|
import { AppSettings } from "../../services/user-app-settings";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
export default function LightningSettings() {
|
export default function LightningSettings() {
|
||||||
const { register } = useFormContext<AppSettings>();
|
const { register, formState } = useFormContext<AppSettings>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccordionItem>
|
<AccordionItem>
|
||||||
@ -31,44 +33,35 @@ export default function LightningSettings() {
|
|||||||
<AccordionPanel>
|
<AccordionPanel>
|
||||||
<Flex direction="column" gap="4">
|
<Flex direction="column" gap="4">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel htmlFor="lightningPayMode" mb="0">
|
<Flex alignItems="center">
|
||||||
Payment mode
|
<FormLabel htmlFor="autoPayWithWebLN" mb="0">
|
||||||
</FormLabel>
|
Auto pay with WebLN
|
||||||
<Select id="lightningPayMode" {...register("lightningPayMode")}>
|
</FormLabel>
|
||||||
<option value="prompt">Prompt</option>
|
<Switch id="autoPayWithWebLN" {...register("autoPayWithWebLN")} />
|
||||||
<option value="webln">WebLN</option>
|
</Flex>
|
||||||
<option value="external">External</option>
|
|
||||||
</Select>
|
|
||||||
<FormHelperText>
|
<FormHelperText>
|
||||||
<span>Prompt: Ask every time</span>
|
<span>Enabled: Attempt to automatically pay with WebLN if its available</span>
|
||||||
<br />
|
|
||||||
<span>WebLN: Use browser extension</span>
|
|
||||||
<br />
|
|
||||||
<span>External: Open an external app using "lightning:" link</span>
|
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel htmlFor="zap-amounts" mb="0">
|
<FormLabel htmlFor="customZapAmounts" mb="0">
|
||||||
Zap Amounts
|
Zap Amounts
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
id="zap-amounts"
|
id="customZapAmounts"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
{...register("zapAmounts", {
|
{...register("customZapAmounts", {
|
||||||
setValueAs: (value: number[] | string) => {
|
validate: (v) => {
|
||||||
if (Array.isArray(value)) {
|
if (!/^[\d,]*$/.test(v)) return "Must be a list of comma separated numbers";
|
||||||
return Array.from(value).join(",");
|
return true;
|
||||||
} else {
|
|
||||||
return value
|
|
||||||
.split(",")
|
|
||||||
.map((v) => parseInt(v))
|
|
||||||
.filter(Boolean)
|
|
||||||
.sort((a, b) => a - b);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
{formState.errors.customZapAmounts && (
|
||||||
|
<FormErrorMessage>{formState.errors.customZapAmounts.message}</FormErrorMessage>
|
||||||
|
)}
|
||||||
<FormHelperText>
|
<FormHelperText>
|
||||||
<span>Comma separated list of custom zap amounts</span>
|
<span>Comma separated list of custom zap amounts</span>
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
|
@ -36,7 +36,7 @@ import RawValue from "../../../components/debug-modals/raw-value";
|
|||||||
import RawJson from "../../../components/debug-modals/raw-json";
|
import RawJson from "../../../components/debug-modals/raw-json";
|
||||||
|
|
||||||
export default function StreamCard({ stream, ...props }: CardProps & { stream: ParsedStream }) {
|
export default function StreamCard({ stream, ...props }: CardProps & { stream: ParsedStream }) {
|
||||||
const { title, summary, identifier, image } = stream;
|
const { title, identifier, image } = stream;
|
||||||
const devModal = useDisclosure();
|
const devModal = useDisclosure();
|
||||||
|
|
||||||
const naddr = useMemo(() => {
|
const naddr = useMemo(() => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@ -11,8 +11,15 @@ import {
|
|||||||
Heading,
|
Heading,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
Spacer,
|
Spacer,
|
||||||
Text,
|
Text,
|
||||||
|
useDisclosure,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { ParsedStream, buildChatMessage, getATag } from "../../../helpers/nostr/stream";
|
import { ParsedStream, buildChatMessage, getATag } from "../../../helpers/nostr/stream";
|
||||||
@ -26,8 +33,15 @@ import { UserLink } from "../../../components/user-link";
|
|||||||
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
||||||
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
import { embedUrls } from "../../../helpers/embeds";
|
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
|
||||||
import { embedEmoji, renderGenericUrl, renderImageUrl } from "../../../components/embed-types";
|
import {
|
||||||
|
embedEmoji,
|
||||||
|
embedNostrHashtags,
|
||||||
|
embedNostrLinks,
|
||||||
|
embedNostrMentions,
|
||||||
|
renderGenericUrl,
|
||||||
|
renderImageUrl,
|
||||||
|
} from "../../../components/embed-types";
|
||||||
import EmbeddedContent from "../../../components/embeded-content";
|
import EmbeddedContent from "../../../components/embeded-content";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useSigningContext } from "../../../providers/signing-provider";
|
import { useSigningContext } from "../../../providers/signing-provider";
|
||||||
@ -41,33 +55,50 @@ import { readablizeSats } from "../../../helpers/bolt11";
|
|||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
import useUserLNURLMetadata from "../../../hooks/use-user-lnurl-metadata";
|
import useUserLNURLMetadata from "../../../hooks/use-user-lnurl-metadata";
|
||||||
import { useInvoiceModalContext } from "../../../providers/invoice-modal";
|
import { useInvoiceModalContext } from "../../../providers/invoice-modal";
|
||||||
|
import { ImageGalleryProvider } from "../../../components/image-gallery";
|
||||||
|
import appSettings from "../../../services/app-settings";
|
||||||
|
import { TrustProvider } from "../../../providers/trust";
|
||||||
|
|
||||||
|
function ChatMessageContent({ event }: { event: NostrEvent }) {
|
||||||
|
const content = useMemo(() => {
|
||||||
|
let c: EmbedableContent = [event.content];
|
||||||
|
|
||||||
|
c = embedUrls(c, [renderImageUrl, renderGenericUrl]);
|
||||||
|
|
||||||
|
// nostr
|
||||||
|
c = embedNostrLinks(c);
|
||||||
|
c = embedNostrMentions(c, event);
|
||||||
|
c = embedNostrHashtags(c, event);
|
||||||
|
c = embedEmoji(c, event);
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}, [event.content]);
|
||||||
|
|
||||||
|
return <EmbeddedContent content={content} />;
|
||||||
|
}
|
||||||
|
|
||||||
function ChatMessage({ event, stream }: { event: NostrEvent; stream: ParsedStream }) {
|
function ChatMessage({ event, stream }: { event: NostrEvent; stream: ParsedStream }) {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useRegisterIntersectionEntity(ref, event.id);
|
useRegisterIntersectionEntity(ref, event.id);
|
||||||
|
|
||||||
const content = useMemo(() => {
|
|
||||||
let c = embedUrls([event.content], [renderImageUrl, renderGenericUrl]);
|
|
||||||
c = embedEmoji(c, event);
|
|
||||||
return c;
|
|
||||||
}, [event.content]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" ref={ref}>
|
<TrustProvider event={event}>
|
||||||
<Flex gap="2" alignItems="center">
|
<Flex direction="column" ref={ref}>
|
||||||
<UserAvatar pubkey={event.pubkey} size="xs" />
|
<Flex gap="2" alignItems="center">
|
||||||
<UserLink
|
<UserAvatar pubkey={event.pubkey} size="xs" />
|
||||||
pubkey={event.pubkey}
|
<UserLink
|
||||||
fontWeight="bold"
|
pubkey={event.pubkey}
|
||||||
color={event.pubkey === stream.author ? "rgb(248, 56, 217)" : "cyan"}
|
fontWeight="bold"
|
||||||
/>
|
color={event.pubkey === stream.author ? "rgb(248, 56, 217)" : "cyan"}
|
||||||
<Spacer />
|
/>
|
||||||
<Text>{dayjs.unix(event.created_at).fromNow()}</Text>
|
<Spacer />
|
||||||
|
<Text>{dayjs.unix(event.created_at).fromNow()}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Box>
|
||||||
|
<ChatMessageContent event={event} />
|
||||||
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box>
|
</TrustProvider>
|
||||||
<EmbeddedContent content={content} />
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,28 +107,24 @@ function ZapMessage({ zap, stream }: { zap: NostrEvent; stream: ParsedStream })
|
|||||||
useRegisterIntersectionEntity(ref, zap.id);
|
useRegisterIntersectionEntity(ref, zap.id);
|
||||||
|
|
||||||
const { request, payment } = parseZapEvent(zap);
|
const { request, payment } = parseZapEvent(zap);
|
||||||
const content = useMemo(() => {
|
|
||||||
let c = embedUrls([request.content], [renderImageUrl, renderGenericUrl]);
|
|
||||||
c = embedEmoji(c, request);
|
|
||||||
return c;
|
|
||||||
}, [request.content]);
|
|
||||||
|
|
||||||
if (!payment.amount) return null;
|
if (!payment.amount) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" borderRadius="md" borderColor="yellow.400" borderWidth="1px" p="2" ref={ref}>
|
<TrustProvider event={request}>
|
||||||
<Flex gap="2">
|
<Flex direction="column" borderRadius="md" borderColor="yellow.400" borderWidth="1px" p="2" ref={ref}>
|
||||||
<LightningIcon color="yellow.400" />
|
<Flex gap="2">
|
||||||
<UserAvatar pubkey={request.pubkey} size="xs" />
|
<LightningIcon color="yellow.400" />
|
||||||
<UserLink pubkey={request.pubkey} fontWeight="bold" color="yellow.400" />
|
<UserAvatar pubkey={request.pubkey} size="xs" />
|
||||||
<Text>zapped {readablizeSats(payment.amount / 1000)} sats</Text>
|
<UserLink pubkey={request.pubkey} fontWeight="bold" color="yellow.400" />
|
||||||
<Spacer />
|
<Text>zapped {readablizeSats(payment.amount / 1000)} sats</Text>
|
||||||
<Text>{dayjs.unix(request.created_at).fromNow()}</Text>
|
<Spacer />
|
||||||
|
<Text>{dayjs.unix(request.created_at).fromNow()}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Box>
|
||||||
|
<ChatMessageContent event={request} />
|
||||||
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box>
|
</TrustProvider>
|
||||||
<EmbeddedContent content={content} />
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +133,7 @@ export default function StreamChat({
|
|||||||
actions,
|
actions,
|
||||||
...props
|
...props
|
||||||
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode }) {
|
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode }) {
|
||||||
|
const { customZapAmounts } = useSubject(appSettings);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const contextRelays = useAdditionalRelayContext();
|
const contextRelays = useAdditionalRelayContext();
|
||||||
const readRelays = useReadRelayUrls(contextRelays);
|
const readRelays = useReadRelayUrls(contextRelays);
|
||||||
@ -135,18 +163,18 @@ export default function StreamChat({
|
|||||||
nostrPostAction(unique([...contextRelays, ...writeRelays]), signed);
|
nostrPostAction(unique([...contextRelays, ...writeRelays]), signed);
|
||||||
reset();
|
reset();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) toast({ description: e.message });
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const zapAmountModal = useDisclosure();
|
||||||
const { requestPay } = useInvoiceModalContext();
|
const { requestPay } = useInvoiceModalContext();
|
||||||
const zapMetadata = useUserLNURLMetadata(stream.author);
|
const zapMetadata = useUserLNURLMetadata(stream.author);
|
||||||
const zapMessage = async () => {
|
const zapMessage = async (amount: number) => {
|
||||||
try {
|
try {
|
||||||
if (!zapMetadata.metadata?.callback) throw new Error("bad lnurl endpoint");
|
if (!zapMetadata.metadata?.callback) throw new Error("bad lnurl endpoint");
|
||||||
|
|
||||||
const content = getValues().content;
|
const content = getValues().content;
|
||||||
const amount = 100;
|
|
||||||
const zapRequest: DraftNostrEvent = {
|
const zapRequest: DraftNostrEvent = {
|
||||||
kind: Kind.ZapRequest,
|
kind: Kind.ZapRequest,
|
||||||
created_at: dayjs().unix(),
|
created_at: dayjs().unix(),
|
||||||
@ -167,53 +195,93 @@ export default function StreamChat({
|
|||||||
|
|
||||||
reset();
|
reset();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) toast({ description: e.message });
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
<>
|
||||||
<Card {...props} overflow="hidden">
|
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
||||||
<CardHeader py="3" display="flex" justifyContent="space-between" alignItems="center">
|
<ImageGalleryProvider>
|
||||||
<Heading size="md">Stream Chat</Heading>
|
<Card {...props} overflow="hidden">
|
||||||
{actions}
|
<CardHeader py="3" display="flex" justifyContent="space-between" alignItems="center">
|
||||||
</CardHeader>
|
<Heading size="md">Stream Chat</Heading>
|
||||||
<CardBody display="flex" flexDirection="column" gap="2" overflow="hidden" p={0}>
|
{actions}
|
||||||
<Flex
|
</CardHeader>
|
||||||
overflowY="scroll"
|
<CardBody display="flex" flexDirection="column" gap="2" overflow="hidden" p={0}>
|
||||||
overflowX="hidden"
|
<Flex
|
||||||
ref={scrollBox}
|
overflowY="scroll"
|
||||||
direction="column-reverse"
|
overflowX="hidden"
|
||||||
flex={1}
|
ref={scrollBox}
|
||||||
px="4"
|
direction="column-reverse"
|
||||||
py="2"
|
flex={1}
|
||||||
gap="2"
|
px="4"
|
||||||
>
|
py="2"
|
||||||
{events.map((event) =>
|
gap="2"
|
||||||
event.kind === 1311 ? (
|
>
|
||||||
<ChatMessage key={event.id} event={event} stream={stream} />
|
{events.map((event) =>
|
||||||
) : (
|
event.kind === 1311 ? (
|
||||||
<ZapMessage key={event.id} zap={event} stream={stream} />
|
<ChatMessage key={event.id} event={event} stream={stream} />
|
||||||
)
|
) : (
|
||||||
)}
|
<ZapMessage key={event.id} zap={event} stream={stream} />
|
||||||
</Flex>
|
)
|
||||||
<Box as="form" borderRadius="md" flexShrink={0} display="flex" gap="2" px="2" pb="2" onSubmit={sendMessage}>
|
)}
|
||||||
<Input placeholder="Message" {...register("content", { required: true })} autoComplete="off" />
|
</Flex>
|
||||||
<Button colorScheme="brand" type="submit" isLoading={formState.isSubmitting}>
|
<Box
|
||||||
Send
|
as="form"
|
||||||
</Button>
|
borderRadius="md"
|
||||||
{zapMetadata.metadata?.allowsNostr && (
|
flexShrink={0}
|
||||||
<IconButton
|
display="flex"
|
||||||
icon={<LightningIcon color="yellow.400" />}
|
gap="2"
|
||||||
aria-label="Zap stream"
|
px="2"
|
||||||
borderColor="yellow.400"
|
pb="2"
|
||||||
variant="outline"
|
onSubmit={sendMessage}
|
||||||
onClick={zapMessage}
|
>
|
||||||
/>
|
<Input placeholder="Message" {...register("content", { required: true })} autoComplete="off" />
|
||||||
)}
|
<Button colorScheme="brand" type="submit" isLoading={formState.isSubmitting}>
|
||||||
</Box>
|
Send
|
||||||
</CardBody>
|
</Button>
|
||||||
</Card>
|
{zapMetadata.metadata?.allowsNostr && (
|
||||||
</IntersectionObserverProvider>
|
<IconButton
|
||||||
|
icon={<LightningIcon color="yellow.400" />}
|
||||||
|
aria-label="Zap stream"
|
||||||
|
borderColor="yellow.400"
|
||||||
|
variant="outline"
|
||||||
|
onClick={zapAmountModal.onOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</ImageGalleryProvider>
|
||||||
|
</IntersectionObserverProvider>
|
||||||
|
<Modal isOpen={zapAmountModal.isOpen} onClose={zapAmountModal.onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader pb="0">Zap Amount</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<Flex gap="2" alignItems="center" flexWrap="wrap">
|
||||||
|
{customZapAmounts
|
||||||
|
.split(",")
|
||||||
|
.map((v) => parseInt(v))
|
||||||
|
.map((amount, i) => (
|
||||||
|
<Button
|
||||||
|
key={amount + i}
|
||||||
|
onClick={() => {
|
||||||
|
zapAmountModal.onClose();
|
||||||
|
zapMessage(amount);
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{amount}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -45,9 +45,7 @@ function EncodeForm() {
|
|||||||
|
|
||||||
setOutput(nprofile);
|
setOutput(nprofile);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
toast({ description: e.message });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -92,9 +90,7 @@ function DecodeForm() {
|
|||||||
try {
|
try {
|
||||||
setOutput(nip19.decode(values.input));
|
setOutput(nip19.decode(values.input));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
toast({ description: e.message });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user