blur images in stream chat

This commit is contained in:
hzrd149 2023-07-02 10:11:30 -05:00
parent 5a537ab9ab
commit e4b40dda68
24 changed files with 258 additions and 225 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Blur images in stream chat

View File

@ -2,7 +2,7 @@ import { Box, Image, ImageProps, Link, useDisclosure } from "@chakra-ui/react";
import appSettings from "../../services/app-settings";
import { ImageGalleryLink } from "../image-gallery";
import { useIsMobile } from "../../hooks/use-is-mobile";
import { useTrusted } from "../note/trust";
import { useTrusted } from "../../providers/trust";
import OpenGraphCard from "../open-graph-card";
const BlurredImage = (props: ImageProps) => {

View File

@ -11,10 +11,9 @@ import {
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { ExternalLinkIcon, LightningIcon, QrCodeIcon } from "./icons";
import { ExternalLinkIcon, QrCodeIcon } from "./icons";
import QrCodeSvg from "./qr-code-svg";
import { CopyIconButton } from "./copy-icon-button";
import { useIsMobile } from "../hooks/use-is-mobile";
export default function InvoiceModal({
invoice,
@ -22,17 +21,20 @@ export default function InvoiceModal({
onPaid,
...props
}: Omit<ModalProps, "children"> & { invoice: string; onPaid: () => void }) {
const isMobile = useIsMobile();
const toast = useToast();
const showQr = useDisclosure();
const payWithWebLn = async (invoice: string) => {
if (window.webln && invoice) {
if (!window.webln.enabled) await window.webln.enable();
await window.webln.sendPayment(invoice);
try {
if (window.webln && invoice) {
if (!window.webln.enabled) await window.webln.enable();
await window.webln.sendPayment(invoice);
if (onPaid) onPaid();
onClose();
if (onPaid) onPaid();
onClose();
}
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
};
const payWithApp = async (invoice: string) => {

View File

@ -37,9 +37,7 @@ export function RepostButton({ event }: { event: NostrEvent }) {
await nostrPostAction(clientRelaysService.getWriteUrls(), repost);
onClose();
} catch (e) {
if (e instanceof Error) {
toast({ status: "error", description: e.message });
}
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
setLoading(false);
};

View File

@ -9,7 +9,7 @@ import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
import useSubject from "../../hooks/use-subject";
import appSettings from "../../services/app-settings";
import EventVerificationIcon from "../event-verification-icon";
import { TrustProvider } from "./trust";
import { TrustProvider } from "../../providers/trust";
import { NoteLink } from "../note-link";
export default function EmbeddedNote({ note }: { note: NostrEvent }) {

View File

@ -32,7 +32,7 @@ import { RepostButton } from "./buttons/repost-button";
import { QuoteRepostButton } from "./buttons/quote-repost-button";
import { ExternalLinkIcon } from "../icons";
import NoteContentWithWarning from "./note-content-with-warning";
import { TrustProvider } from "./trust";
import { TrustProvider } from "../../providers/trust";
import { NoteLink } from "../note-link";
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";

View File

@ -21,11 +21,10 @@ import {
renderOpenGraphUrl,
} from "../embed-types";
import { ImageGalleryProvider } from "../image-gallery";
import { useTrusted } from "./trust";
import { renderRedditUrl } from "../embed-types/reddit";
import EmbeddedContent from "../embeded-content";
function buildContents(event: NostrEvent | DraftNostrEvent, trusted = false) {
function buildContents(event: NostrEvent | DraftNostrEvent) {
let content: EmbedableContent = [event.content.trim()];
// common
@ -70,8 +69,7 @@ export type NoteContentsProps = {
};
export const NoteContents = React.memo(({ event, maxHeight }: NoteContentsProps) => {
const trusted = useTrusted();
const content = buildContents(event, trusted);
const content = buildContents(event);
const expand = useExpand();
const [innerHeight, setInnerHeight] = useState(0);
const ref = useRef<HTMLDivElement | null>(null);

View File

@ -53,12 +53,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
await results.onComplete;
deleteModal.onClose();
} catch (e) {
if (e instanceof Error) {
toast({
status: "error",
description: e.message,
});
}
if (e instanceof Error) toast({ description: e.message, status: "error" });
} finally {
setDeleting(false);
}

View File

@ -27,7 +27,7 @@ import { ImageIcon } from "../icons";
import { NoteLink } from "../note-link";
import { NoteContents } from "../note/note-contents";
import { PostResults } from "./post-results";
import { TrustProvider } from "../note/trust";
import { TrustProvider } from "../../providers/trust";
function emptyDraft(): DraftNostrEvent {
return {
@ -96,12 +96,7 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
setDraft((d) => ({ ...d, content: (d.content += imageUrl) }));
}
} catch (e) {
if (e instanceof Error) {
toast({
status: "error",
description: e.message,
});
}
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
setUploading(false);
};

View File

@ -9,7 +9,7 @@ import { NoteMenu } from "./note/note-menu";
import { UserAvatar } from "./user-avatar";
import { UserDnsIdentityIcon } from "./user-dns-identity-icon";
import { UserLink } from "./user-link";
import { TrustProvider } from "./note/trust";
import { TrustProvider } from "../providers/trust";
import { safeJson } from "../helpers/parse";
import { verifySignature } from "nostr-tools";
import { useReadRelayUrls } from "../hooks/use-client-relays";

View File

@ -59,13 +59,13 @@ export default function ZapModal({
initialAmount,
...props
}: ZapModalProps) {
const isMobile = useIsMobile();
const metadata = useUserMetadata(pubkey);
const { requestSignature } = useSigningContext();
const toast = useToast();
const [promptInvoice, setPromptInvoice] = useState<string>();
const { isOpen: showQr, onToggle: toggleQr } = useDisclosure();
const isMobile = useIsMobile();
const { zapAmounts } = useSubject(appSettings);
const { customZapAmounts } = useSubject(appSettings);
const {
register,
@ -76,7 +76,7 @@ export default function ZapModal({
} = useForm<FormValues>({
mode: "onBlur",
defaultValues: {
amount: initialAmount ?? zapAmounts[0],
amount: initialAmount ?? (parseInt(customZapAmounts.split(",")[0]) || 100),
comment: initialComment ?? "",
},
});
@ -160,19 +160,11 @@ export default function ZapModal({
}, 1000 * 2);
};
const payInvoice = (invoice: string) => {
switch (appSettings.value.lightningPayMode) {
case "webln":
payWithWebLn(invoice);
break;
case "external":
payWithApp(invoice);
break;
default:
case "prompt":
setPromptInvoice(invoice);
break;
const payInvoice = async (invoice: string) => {
if (appSettings.value.autoPayWithWebLN) {
await payWithWebLn(invoice);
}
setPromptInvoice(invoice);
};
const handleClose = () => {
@ -228,11 +220,14 @@ export default function ZapModal({
<UserLink pubkey={pubkey} />
</Flex>
<Flex gap="2" alignItems="center" flexWrap="wrap">
{zapAmounts.map((amount, i) => (
<Button key={amount + i} onClick={() => setValue("amount", amount)} size="sm" variant="outline">
{amount}
</Button>
))}
{customZapAmounts
.split(",")
.map((v) => parseInt(v))
.map((amount, i) => (
<Button key={amount + i} onClick={() => setValue("amount", amount)} size="sm" variant="outline">
{amount}
</Button>
))}
</Flex>
<Flex gap="2">
<InputGroup maxWidth={32}>

View File

@ -13,12 +13,7 @@ export default function useAppSettings() {
try {
return replaceSettings({ ...settings, ...newSettings });
} catch (e) {
if (e instanceof Error) {
toast({
status: "error",
description: e.message,
});
}
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
},
[settings]

View File

@ -1,6 +1,7 @@
import React, { useCallback, useContext, useState } from "react";
import InvoiceModal from "../components/invoice-modal";
import createDefer, { Deferred } from "../classes/deferred";
import appSettings from "../services/app-settings";
export type InvoiceModalContext = {
requestPay: (invoice: string) => Promise<void>;
@ -20,7 +21,17 @@ export const InvoiceModalProvider = ({ children }: { children: React.ReactNode }
const [invoice, setInvoice] = useState<string>();
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>();
setDefer(defer);
setInvoice(invoice);

View File

@ -37,12 +37,7 @@ export const SigningProvider = ({ children }: { children: React.ReactNode }) =>
if (!current) throw new Error("No account");
return await signingService.requestSignature(draft, current);
} catch (e) {
if (e instanceof Error) {
toast({
status: "error",
description: e.message,
});
}
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
},
[toast, current]
@ -53,12 +48,7 @@ export const SigningProvider = ({ children }: { children: React.ReactNode }) =>
if (!current) throw new Error("No account");
return await signingService.requestDecrypt(data, pubkey, current);
} catch (e) {
if (e instanceof Error) {
toast({
status: "error",
description: e.message,
});
}
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
},
[toast, current]
@ -69,12 +59,7 @@ export const SigningProvider = ({ children }: { children: React.ReactNode }) =>
if (!current) throw new Error("No account");
return await signingService.requestEncrypt(data, pubkey, current);
} catch (e) {
if (e instanceof Error) {
toast({
status: "error",
description: e.message,
});
}
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
},
[toast, current]

View File

@ -1,8 +1,8 @@
import React, { PropsWithChildren, useContext } from "react";
import { NostrEvent } from "../../types/nostr-event";
import { useCurrentAccount } from "../../hooks/use-current-account";
import clientFollowingService from "../../services/client-following";
import useSubject from "../../hooks/use-subject";
import { NostrEvent } from "../types/nostr-event";
import { useCurrentAccount } from "../hooks/use-current-account";
import clientFollowingService from "../services/client-following";
import useSubject from "../hooks/use-subject";
const TrustContext = React.createContext<boolean>(false);

View File

@ -9,12 +9,6 @@ import db from "./db";
const DTAG = "nostrudel-settings";
export enum LightningPayMode {
Prompt = "prompt",
Webln = "webln",
External = "external",
}
export type AppSettings = {
colorMode: ColorMode;
blurImages: boolean;
@ -22,8 +16,10 @@ export type AppSettings = {
proxyUserMedia: boolean;
showReactions: boolean;
showSignatureVerification: boolean;
lightningPayMode: LightningPayMode;
zapAmounts: number[];
autoPayWithWebLN: boolean;
customZapAmounts: string;
primaryColor: string;
imageProxy: string;
corsProxy: string;
@ -40,8 +36,10 @@ export const defaultSettings: AppSettings = {
proxyUserMedia: false,
showReactions: true,
showSignatureVerification: false,
lightningPayMode: LightningPayMode.Prompt,
zapAmounts: [50, 200, 500, 1000],
autoPayWithWebLN: true,
customZapAmounts: "50,200,500,1000,2000,5000",
primaryColor: "#8DB600",
imageProxy: "",
corsProxy: "",

View File

@ -4,7 +4,7 @@ import { ArrowDownSIcon, ArrowUpSIcon } from "../../components/icons";
import { Note } from "../../components/note";
import { countReplies, ThreadItem as ThreadItemData } from "../../helpers/thread";
import { useIsMobile } from "../../hooks/use-is-mobile";
import { TrustProvider } from "../../components/note/trust";
import { TrustProvider } from "../../providers/trust";
export type ThreadItemProps = {
post: ThreadItemData;

View File

@ -243,12 +243,7 @@ export const ProfileEditView = () => {
await results.onComplete;
} catch (e) {
if (e instanceof Error) {
toast({
status: "error",
description: e.message,
});
}
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
};

View File

@ -61,9 +61,7 @@ function RelaysPage() {
}
setRelayInputValue("");
} catch (e) {
if (e instanceof Error) {
toast({ status: "error", description: e.message });
}
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
};
const savePending = async () => {

View File

@ -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 { GithubIcon, ToolsIcon } from "../../components/icons";
import LightningSettings from "./lightning-settings";
@ -10,6 +10,7 @@ import useAppSettings from "../../hooks/use-app-settings";
import { FormProvider, useForm } from "react-hook-form";
export default function SettingsView() {
const toast = useToast();
const { updateSettings, ...settings } = useAppSettings();
const form = useForm({
@ -18,7 +19,12 @@ export default function SettingsView() {
});
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 (

View File

@ -10,13 +10,15 @@ import {
FormHelperText,
Input,
Select,
Switch,
FormErrorMessage,
} from "@chakra-ui/react";
import { LightningIcon } from "../../components/icons";
import { AppSettings } from "../../services/user-app-settings";
import { useFormContext } from "react-hook-form";
export default function LightningSettings() {
const { register } = useFormContext<AppSettings>();
const { register, formState } = useFormContext<AppSettings>();
return (
<AccordionItem>
@ -31,44 +33,35 @@ export default function LightningSettings() {
<AccordionPanel>
<Flex direction="column" gap="4">
<FormControl>
<FormLabel htmlFor="lightningPayMode" mb="0">
Payment mode
</FormLabel>
<Select id="lightningPayMode" {...register("lightningPayMode")}>
<option value="prompt">Prompt</option>
<option value="webln">WebLN</option>
<option value="external">External</option>
</Select>
<Flex alignItems="center">
<FormLabel htmlFor="autoPayWithWebLN" mb="0">
Auto pay with WebLN
</FormLabel>
<Switch id="autoPayWithWebLN" {...register("autoPayWithWebLN")} />
</Flex>
<FormHelperText>
<span>Prompt: Ask every time</span>
<br />
<span>WebLN: Use browser extension</span>
<br />
<span>External: Open an external app using "lightning:" link</span>
<span>Enabled: Attempt to automatically pay with WebLN if its available</span>
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel htmlFor="zap-amounts" mb="0">
<FormLabel htmlFor="customZapAmounts" mb="0">
Zap Amounts
</FormLabel>
<Input
id="zap-amounts"
id="customZapAmounts"
autoComplete="off"
{...register("zapAmounts", {
setValueAs: (value: number[] | string) => {
if (Array.isArray(value)) {
return Array.from(value).join(",");
} else {
return value
.split(",")
.map((v) => parseInt(v))
.filter(Boolean)
.sort((a, b) => a - b);
}
{...register("customZapAmounts", {
validate: (v) => {
if (!/^[\d,]*$/.test(v)) return "Must be a list of comma separated numbers";
return true;
},
})}
/>
{formState.errors.customZapAmounts && (
<FormErrorMessage>{formState.errors.customZapAmounts.message}</FormErrorMessage>
)}
<FormHelperText>
<span>Comma separated list of custom zap amounts</span>
</FormHelperText>

View File

@ -36,7 +36,7 @@ import RawValue from "../../../components/debug-modals/raw-value";
import RawJson from "../../../components/debug-modals/raw-json";
export default function StreamCard({ stream, ...props }: CardProps & { stream: ParsedStream }) {
const { title, summary, identifier, image } = stream;
const { title, identifier, image } = stream;
const devModal = useDisclosure();
const naddr = useMemo(() => {

View File

@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef } from "react";
import { useMemo, useRef } from "react";
import dayjs from "dayjs";
import {
Box,
@ -11,8 +11,15 @@ import {
Heading,
IconButton,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
Spacer,
Text,
useDisclosure,
useToast,
} from "@chakra-ui/react";
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 IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import { embedUrls } from "../../../helpers/embeds";
import { embedEmoji, renderGenericUrl, renderImageUrl } from "../../../components/embed-types";
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
import {
embedEmoji,
embedNostrHashtags,
embedNostrLinks,
embedNostrMentions,
renderGenericUrl,
renderImageUrl,
} from "../../../components/embed-types";
import EmbeddedContent from "../../../components/embeded-content";
import { useForm } from "react-hook-form";
import { useSigningContext } from "../../../providers/signing-provider";
@ -41,33 +55,50 @@ import { readablizeSats } from "../../../helpers/bolt11";
import { Kind } from "nostr-tools";
import useUserLNURLMetadata from "../../../hooks/use-user-lnurl-metadata";
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 }) {
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, event.id);
const content = useMemo(() => {
let c = embedUrls([event.content], [renderImageUrl, renderGenericUrl]);
c = embedEmoji(c, event);
return c;
}, [event.content]);
return (
<Flex direction="column" ref={ref}>
<Flex gap="2" alignItems="center">
<UserAvatar pubkey={event.pubkey} size="xs" />
<UserLink
pubkey={event.pubkey}
fontWeight="bold"
color={event.pubkey === stream.author ? "rgb(248, 56, 217)" : "cyan"}
/>
<Spacer />
<Text>{dayjs.unix(event.created_at).fromNow()}</Text>
<TrustProvider event={event}>
<Flex direction="column" ref={ref}>
<Flex gap="2" alignItems="center">
<UserAvatar pubkey={event.pubkey} size="xs" />
<UserLink
pubkey={event.pubkey}
fontWeight="bold"
color={event.pubkey === stream.author ? "rgb(248, 56, 217)" : "cyan"}
/>
<Spacer />
<Text>{dayjs.unix(event.created_at).fromNow()}</Text>
</Flex>
<Box>
<ChatMessageContent event={event} />
</Box>
</Flex>
<Box>
<EmbeddedContent content={content} />
</Box>
</Flex>
</TrustProvider>
);
}
@ -76,28 +107,24 @@ function ZapMessage({ zap, stream }: { zap: NostrEvent; stream: ParsedStream })
useRegisterIntersectionEntity(ref, zap.id);
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;
return (
<Flex direction="column" borderRadius="md" borderColor="yellow.400" borderWidth="1px" p="2" ref={ref}>
<Flex gap="2">
<LightningIcon color="yellow.400" />
<UserAvatar pubkey={request.pubkey} size="xs" />
<UserLink pubkey={request.pubkey} fontWeight="bold" color="yellow.400" />
<Text>zapped {readablizeSats(payment.amount / 1000)} sats</Text>
<Spacer />
<Text>{dayjs.unix(request.created_at).fromNow()}</Text>
<TrustProvider event={request}>
<Flex direction="column" borderRadius="md" borderColor="yellow.400" borderWidth="1px" p="2" ref={ref}>
<Flex gap="2">
<LightningIcon color="yellow.400" />
<UserAvatar pubkey={request.pubkey} size="xs" />
<UserLink pubkey={request.pubkey} fontWeight="bold" color="yellow.400" />
<Text>zapped {readablizeSats(payment.amount / 1000)} sats</Text>
<Spacer />
<Text>{dayjs.unix(request.created_at).fromNow()}</Text>
</Flex>
<Box>
<ChatMessageContent event={request} />
</Box>
</Flex>
<Box>
<EmbeddedContent content={content} />
</Box>
</Flex>
</TrustProvider>
);
}
@ -106,6 +133,7 @@ export default function StreamChat({
actions,
...props
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode }) {
const { customZapAmounts } = useSubject(appSettings);
const toast = useToast();
const contextRelays = useAdditionalRelayContext();
const readRelays = useReadRelayUrls(contextRelays);
@ -135,18 +163,18 @@ export default function StreamChat({
nostrPostAction(unique([...contextRelays, ...writeRelays]), signed);
reset();
} 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 zapMetadata = useUserLNURLMetadata(stream.author);
const zapMessage = async () => {
const zapMessage = async (amount: number) => {
try {
if (!zapMetadata.metadata?.callback) throw new Error("bad lnurl endpoint");
const content = getValues().content;
const amount = 100;
const zapRequest: DraftNostrEvent = {
kind: Kind.ZapRequest,
created_at: dayjs().unix(),
@ -167,53 +195,93 @@ export default function StreamChat({
reset();
} catch (e) {
if (e instanceof Error) toast({ description: e.message });
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
};
return (
<IntersectionObserverProvider callback={callback} root={scrollBox}>
<Card {...props} overflow="hidden">
<CardHeader py="3" display="flex" justifyContent="space-between" alignItems="center">
<Heading size="md">Stream Chat</Heading>
{actions}
</CardHeader>
<CardBody display="flex" flexDirection="column" gap="2" overflow="hidden" p={0}>
<Flex
overflowY="scroll"
overflowX="hidden"
ref={scrollBox}
direction="column-reverse"
flex={1}
px="4"
py="2"
gap="2"
>
{events.map((event) =>
event.kind === 1311 ? (
<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" />
<Button colorScheme="brand" type="submit" isLoading={formState.isSubmitting}>
Send
</Button>
{zapMetadata.metadata?.allowsNostr && (
<IconButton
icon={<LightningIcon color="yellow.400" />}
aria-label="Zap stream"
borderColor="yellow.400"
variant="outline"
onClick={zapMessage}
/>
)}
</Box>
</CardBody>
</Card>
</IntersectionObserverProvider>
<>
<IntersectionObserverProvider callback={callback} root={scrollBox}>
<ImageGalleryProvider>
<Card {...props} overflow="hidden">
<CardHeader py="3" display="flex" justifyContent="space-between" alignItems="center">
<Heading size="md">Stream Chat</Heading>
{actions}
</CardHeader>
<CardBody display="flex" flexDirection="column" gap="2" overflow="hidden" p={0}>
<Flex
overflowY="scroll"
overflowX="hidden"
ref={scrollBox}
direction="column-reverse"
flex={1}
px="4"
py="2"
gap="2"
>
{events.map((event) =>
event.kind === 1311 ? (
<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" />
<Button colorScheme="brand" type="submit" isLoading={formState.isSubmitting}>
Send
</Button>
{zapMetadata.metadata?.allowsNostr && (
<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>
</>
);
}

View File

@ -45,9 +45,7 @@ function EncodeForm() {
setOutput(nprofile);
} catch (e) {
if (e instanceof Error) {
toast({ description: e.message });
}
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
});
@ -92,9 +90,7 @@ function DecodeForm() {
try {
setOutput(nip19.decode(values.input));
} catch (e) {
if (e instanceof Error) {
toast({ description: e.message });
}
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
});