clean up zap modal

show stream image in zap modal
This commit is contained in:
hzrd149 2023-07-02 11:34:50 -05:00
parent 871d6994e0
commit 33acce589e
11 changed files with 162 additions and 234 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Fixed bug with stream loading wrong chat

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
UX improvements to zap modal

View File

@ -101,7 +101,7 @@ export const LinkItem = createIcon({
export const LightningIcon = createIcon({ export const LightningIcon = createIcon({
displayName: "LightningIcon", displayName: "LightningIcon",
d: "M13 10h7l-9 13v-9H4l9-13z", d: "M13 10h7l-9 13v-9H4l9-13z",
defaultProps, defaultProps: { ...defaultProps, color: "yellow.400" },
}); });
export const RelayIcon = createIcon({ export const RelayIcon = createIcon({
@ -263,5 +263,5 @@ export const AtIcon = createIcon({
export const LiveStreamIcon = createIcon({ export const LiveStreamIcon = createIcon({
displayName: "LiveStreamIcon", displayName: "LiveStreamIcon",
d: "M16 4C16.5523 4 17 4.44772 17 5V9.2L22.2133 5.55071C22.4395 5.39235 22.7513 5.44737 22.9096 5.6736C22.9684 5.75764 23 5.85774 23 5.96033V18.0397C23 18.3158 22.7761 18.5397 22.5 18.5397C22.3974 18.5397 22.2973 18.5081 22.2133 18.4493L17 14.8V19C17 19.5523 16.5523 20 16 20H2C1.44772 20 1 19.5523 1 19V5C1 4.44772 1.44772 4 2 4H16ZM15 6H3V18H15V6ZM7.4 8.82867C7.47607 8.82867 7.55057 8.85036 7.61475 8.8912L11.9697 11.6625C12.1561 11.7811 12.211 12.0284 12.0924 12.2148C12.061 12.2641 12.0191 12.306 11.9697 12.3375L7.61475 15.1088C7.42837 15.2274 7.18114 15.1725 7.06254 14.9861C7.02169 14.9219 7 14.8474 7 14.7713V9.22867C7 9.00776 7.17909 8.82867 7.4 8.82867ZM21 8.84131L17 11.641V12.359L21 15.1587V8.84131Z", d: "M16 4C16.5523 4 17 4.44772 17 5V9.2L22.2133 5.55071C22.4395 5.39235 22.7513 5.44737 22.9096 5.6736C22.9684 5.75764 23 5.85774 23 5.96033V18.0397C23 18.3158 22.7761 18.5397 22.5 18.5397C22.3974 18.5397 22.2973 18.5081 22.2133 18.4493L17 14.8V19C17 19.5523 16.5523 20 16 20H2C1.44772 20 1 19.5523 1 19V5C1 4.44772 1.44772 4 2 4H16ZM15 6H3V18H15V6ZM7.4 8.82867C7.47607 8.82867 7.55057 8.85036 7.61475 8.8912L11.9697 11.6625C12.1561 11.7811 12.211 12.0284 12.0924 12.2148C12.061 12.2641 12.0191 12.306 11.9697 12.3375L7.61475 15.1088C7.42837 15.2274 7.18114 15.1725 7.06254 14.9861C7.02169 14.9219 7 14.8474 7 14.7713V9.22867C7 9.00776 7.17909 8.82867 7.4 8.82867ZM21 8.84131L17 11.641V12.359L21 15.1587V8.84131Z",
defaultProps defaultProps,
}) });

View File

@ -31,7 +31,6 @@ export default function InvoiceModal({
await window.webln.sendPayment(invoice); await window.webln.sendPayment(invoice);
if (onPaid) onPaid(); if (onPaid) onPaid();
onClose();
} }
} catch (e) { } catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" }); if (e instanceof Error) toast({ description: e.message, status: "error" });

View File

@ -10,10 +10,12 @@ import eventZapsService from "../../services/event-zaps";
import { NostrEvent } from "../../types/nostr-event"; import { NostrEvent } from "../../types/nostr-event";
import { LightningIcon } from "../icons"; import { LightningIcon } from "../icons";
import ZapModal from "../zap-modal"; import ZapModal from "../zap-modal";
import { useInvoiceModalContext } from "../../providers/invoice-modal";
export default function NoteZapButton({ note, ...props }: { note: NostrEvent } & Omit<ButtonProps, "children">) { export default function NoteZapButton({ note, ...props }: { note: NostrEvent } & Omit<ButtonProps, "children">) {
const account = useCurrentAccount(); const account = useCurrentAccount();
const metadata = useUserMetadata(note.pubkey); const metadata = useUserMetadata(note.pubkey);
const { requestPay } = useInvoiceModalContext();
const zaps = useEventZaps(note.id) ?? []; const zaps = useEventZaps(note.id) ?? [];
const parsedZaps = useMemo(() => { const parsedZaps = useMemo(() => {
const parsed = []; const parsed = [];
@ -29,7 +31,11 @@ export default function NoteZapButton({ note, ...props }: { note: NostrEvent } &
const hasZapped = !!account && parsedZaps.some((zapRequest) => zapRequest.request.pubkey === account.pubkey); const hasZapped = !!account && parsedZaps.some((zapRequest) => zapRequest.request.pubkey === account.pubkey);
const tipAddress = metadata?.lud06 || metadata?.lud16; const tipAddress = metadata?.lud06 || metadata?.lud16;
const invoicePaid = () => eventZapsService.requestZaps(note.id, clientRelaysService.getReadUrls(), true); const handleInvoice = async (invoice: string) => {
onClose();
await requestPay(invoice);
eventZapsService.requestZaps(note.id, clientRelaysService.getReadUrls(), true);
};
return ( return (
<> <>
@ -44,7 +50,9 @@ export default function NoteZapButton({ note, ...props }: { note: NostrEvent } &
> >
{readablizeSats(totalZaps(zaps) / 1000)} {readablizeSats(totalZaps(zaps) / 1000)}
</Button> </Button>
{isOpen && <ZapModal isOpen={isOpen} onClose={onClose} event={note} onPaid={invoicePaid} pubkey={note.pubkey} />} {isOpen && (
<ZapModal isOpen={isOpen} onClose={onClose} event={note} onInvoice={handleInvoice} pubkey={note.pubkey} />
)}
</> </>
); );
} }

View File

@ -1,6 +1,6 @@
import { useReadRelayUrls } from "../../hooks/use-client-relays"; import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useSingleEvent from "../../hooks/use-single-event"; import useSingleEvent from "../../hooks/use-single-event";
import EmbeddedNote from "./embeded-note"; import EmbeddedNote from "./embedded-note";
import { NoteLink } from "../note-link"; import { NoteLink } from "../note-link";
const QuoteNote = ({ noteId, relay }: { noteId: string; relay?: string }) => { const QuoteNote = ({ noteId, relay }: { noteId: string; relay?: string }) => {

View File

@ -1,13 +1,13 @@
import { IconButton, IconButtonProps, useDisclosure, useToast } from "@chakra-ui/react"; import { IconButton, IconButtonProps, useDisclosure } from "@chakra-ui/react";
import { useUserMetadata } from "../hooks/use-user-metadata"; import { useUserMetadata } from "../hooks/use-user-metadata";
import { LightningIcon } from "./icons"; import { LightningIcon } from "./icons";
import { useState } from "react";
import { encodeText } from "../helpers/bech32";
import ZapModal from "./zap-modal"; import ZapModal from "./zap-modal";
import { useInvoiceModalContext } from "../providers/invoice-modal";
export const UserTipButton = ({ pubkey, ...props }: { pubkey: string } & Omit<IconButtonProps, "aria-label">) => { export const UserTipButton = ({ pubkey, ...props }: { pubkey: string } & Omit<IconButtonProps, "aria-label">) => {
const metadata = useUserMetadata(pubkey); const metadata = useUserMetadata(pubkey);
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { requestPay } = useInvoiceModalContext();
if (!metadata) return null; if (!metadata) return null;
// use lud06 and lud16 fields interchangeably // use lud06 and lud16 fields interchangeably
@ -25,7 +25,17 @@ export const UserTipButton = ({ pubkey, ...props }: { pubkey: string } & Omit<Ic
color="yellow.400" color="yellow.400"
{...props} {...props}
/> />
{isOpen && <ZapModal isOpen={isOpen} onClose={onClose} pubkey={pubkey} />} {isOpen && (
<ZapModal
isOpen={isOpen}
onClose={onClose}
pubkey={pubkey}
onInvoice={async (invoice) => {
await requestPay(invoice);
onClose();
}}
/>
)}
</> </>
); );
}; };

View File

@ -1,39 +1,40 @@
import { import {
Box,
Button, Button,
Flex, Flex,
IconButton, Heading,
Image,
Input, Input,
InputGroup,
InputLeftElement,
Modal, Modal,
ModalBody, ModalBody,
ModalCloseButton,
ModalContent, ModalContent,
ModalOverlay, ModalOverlay,
ModalProps, ModalProps,
Text, Text,
useDisclosure,
useToast, useToast,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useState } from "react"; import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
import { getUserDisplayName } from "../helpers/user-metadata"; import { useForm } from "react-hook-form";
import { NostrEvent } from "../types/nostr-event";
import { SubmitHandler, useForm } from "react-hook-form";
import { UserAvatar } from "./user-avatar"; import { UserAvatar } from "./user-avatar";
import { useUserMetadata } from "../hooks/use-user-metadata";
import { UserLink } from "./user-link"; import { UserLink } from "./user-link";
import { parsePaymentRequest, readablizeSats } from "../helpers/bolt11"; import { parsePaymentRequest, readablizeSats } from "../helpers/bolt11";
import { ExternalLinkIcon, LightningIcon, QrCodeIcon } from "./icons"; import { LightningIcon } from "./icons";
import { nip57 } from "nostr-tools"; import { Kind } from "nostr-tools";
import clientRelaysService from "../services/client-relays"; import clientRelaysService from "../services/client-relays";
import { getEventRelays } from "../services/event-relays"; import { getEventRelays } from "../services/event-relays";
import { useSigningContext } from "../providers/signing-provider"; import { useSigningContext } from "../providers/signing-provider";
import QrCodeSvg from "./qr-code-svg";
import { CopyIconButton } from "./copy-icon-button";
import { useIsMobile } from "../hooks/use-is-mobile";
import appSettings from "../services/app-settings"; import appSettings from "../services/app-settings";
import useSubject from "../hooks/use-subject"; import useSubject from "../hooks/use-subject";
import useUserLNURLMetadata from "../hooks/use-user-lnurl-metadata"; import useUserLNURLMetadata from "../hooks/use-user-lnurl-metadata";
import { requestZapInvoice } from "../helpers/zaps"; import { requestZapInvoice } from "../helpers/zaps";
import { ParsedStream, getATag } from "../helpers/nostr/stream";
import EmbeddedNote from "./note/embedded-note";
import dayjs from "dayjs";
import { unique } from "../helpers/array";
import { useUserRelays } from "../hooks/use-user-relays";
import { RelayMode } from "../classes/relay";
import relayScoreboardService from "../services/relay-scoreboard";
type FormValues = { type FormValues = {
amount: number; amount: number;
@ -41,29 +42,30 @@ type FormValues = {
}; };
export type ZapModalProps = Omit<ModalProps, "children"> & { export type ZapModalProps = Omit<ModalProps, "children"> & {
event?: NostrEvent;
pubkey: string; pubkey: string;
onPaid?: () => void; event?: NostrEvent;
stream?: ParsedStream;
initialComment?: string; initialComment?: string;
initialAmount?: number; initialAmount?: number;
onInvoice: (invoice: string) => void;
}; };
export default function ZapModal({ export default function ZapModal({
event, event,
pubkey, pubkey,
stream,
onClose, onClose,
onPaid,
initialComment, initialComment,
initialAmount, initialAmount,
onInvoice,
...props ...props
}: ZapModalProps) { }: ZapModalProps) {
const isMobile = useIsMobile();
const metadata = useUserMetadata(pubkey);
const { requestSignature } = useSigningContext();
const toast = useToast(); const toast = useToast();
const [promptInvoice, setPromptInvoice] = useState<string>(); const { requestSignature } = useSigningContext();
const { isOpen: showQr, onToggle: toggleQr } = useDisclosure();
const { customZapAmounts } = useSubject(appSettings); const { customZapAmounts } = useSubject(appSettings);
const userReadRelays = useUserRelays(pubkey)
.filter((r) => r.mode & RelayMode.READ)
.map((r) => r.url);
const { const {
register, register,
@ -84,7 +86,7 @@ export default function ZapModal({
const canZap = lnurlMetadata?.allowsNostr && lnurlMetadata?.nostrPubkey; const canZap = lnurlMetadata?.allowsNostr && lnurlMetadata?.nostrPubkey;
const actionName = canZap ? "Zap" : "Tip"; const actionName = canZap ? "Zap" : "Tip";
const onSubmitZap: SubmitHandler<FormValues> = async (values) => { const onSubmitZap = handleSubmit(async (values) => {
try { try {
if (!tipAddress) throw new Error("No lightning address"); if (!tipAddress) throw new Error("No lightning address");
if (lnurlMetadata) { if (lnurlMetadata) {
@ -93,21 +95,29 @@ export default function ZapModal({
if (amountInMilisat > lnurlMetadata.maxSendable) throw new Error("amount to large"); if (amountInMilisat > lnurlMetadata.maxSendable) throw new Error("amount to large");
if (amountInMilisat < lnurlMetadata.minSendable) throw new Error("amount to small"); if (amountInMilisat < lnurlMetadata.minSendable) throw new Error("amount to small");
if (canZap) { if (canZap) {
const otherRelays = event ? getEventRelays(event.id).value : []; const eventRelays = event ? getEventRelays(event.id).value : [];
const readRelays = clientRelaysService.getReadUrls(); const eventRelaysRanked = relayScoreboardService.getRankedRelays(eventRelays).slice(0, 4);
const writeRelays = clientRelaysService.getWriteUrls();
const writeRelaysRanked = relayScoreboardService.getRankedRelays(writeRelays).slice(0, 4);
const userReadRelaysRanked = relayScoreboardService.getRankedRelays(userReadRelays).slice(0, 4);
const zapRequest = nip57.makeZapRequest({ const zapRequest: DraftNostrEvent = {
profile: pubkey, kind: Kind.ZapRequest,
event: event?.id ?? null, created_at: dayjs().unix(),
relays: [...otherRelays, ...readRelays], content: values.comment,
amount: amountInMilisat, tags: [
comment: values.comment, ["p", pubkey],
}); ["relays", ...unique([...writeRelaysRanked, ...userReadRelaysRanked, ...eventRelaysRanked])],
["amount", String(amountInMilisat)],
],
};
if (event) zapRequest.tags.push(["e", event.id]);
if (stream) zapRequest.tags.push(["a", getATag(stream)]);
const signed = await requestSignature(zapRequest); const signed = await requestSignature(zapRequest);
if (signed) { if (signed) {
const payRequest = await requestZapInvoice(signed, lnurlMetadata.callback); const payRequest = await requestZapInvoice(signed, lnurlMetadata.callback);
payInvoice(payRequest); await onInvoice(payRequest);
} }
} else { } else {
const callbackUrl = new URL(lnurlMetadata.callback); const callbackUrl = new URL(lnurlMetadata.callback);
@ -119,143 +129,83 @@ export default function ZapModal({
const parsed = parsePaymentRequest(payRequest); const parsed = parsePaymentRequest(payRequest);
if (parsed.amount !== amountInMilisat) throw new Error("incorrect amount"); if (parsed.amount !== amountInMilisat) throw new Error("incorrect amount");
payInvoice(payRequest); await onInvoice(payRequest);
} else throw new Error("Failed to get invoice"); } else throw new Error("Failed to get invoice");
} }
} else throw new Error("Failed to get LNURL metadata"); } else throw new Error("Failed to get LNURL metadata");
} catch (e) { } catch (e) {
if (e instanceof Error) toast({ status: "error", description: e.message }); if (e instanceof Error) toast({ status: "error", description: e.message });
} }
};
const payWithWebLn = async (invoice: string) => {
if (window.webln && invoice) {
if (!window.webln.enabled) await window.webln.enable();
await window.webln.sendPayment(invoice);
toast({
title: actionName + " sent",
status: "success",
duration: 3000,
}); });
if (onPaid) onPaid();
onClose();
}
};
const payWithApp = async (invoice: string) => {
window.open("lightning:" + invoice);
const listener = () => {
if (document.visibilityState === "visible") {
if (onPaid) onPaid();
onClose();
document.removeEventListener("visibilitychange", listener);
}
};
setTimeout(() => {
document.addEventListener("visibilitychange", listener);
}, 1000 * 2);
};
const payInvoice = async (invoice: string) => {
if (appSettings.value.autoPayWithWebLN) {
await payWithWebLn(invoice);
}
setPromptInvoice(invoice);
};
const handleClose = () => {
// if there was an invoice and we are closing the modal. presume it was paid
if (promptInvoice && onPaid) {
onPaid();
}
onClose();
};
return ( return (
<Modal onClose={handleClose} {...props}> <Modal onClose={onClose} size="xl" {...props}>
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
<ModalCloseButton />
<ModalBody padding="4"> <ModalBody padding="4">
{promptInvoice ? ( <form onSubmit={onSubmitZap}>
<Flex gap="4" direction="column">
{showQr && <QrCodeSvg content={promptInvoice} />}
<Flex gap="2">
<Input value={promptInvoice} readOnly />
<IconButton
icon={<QrCodeIcon />}
aria-label="Show QrCode"
onClick={toggleQr}
variant="solid"
size="md"
/>
<CopyIconButton text={promptInvoice} aria-label="Copy Invoice" variant="solid" size="md" />
</Flex>
<Flex gap="2">
{window.webln && (
<Button onClick={() => payWithWebLn(promptInvoice)} flex={1} variant="solid" size="md">
Pay with WebLN
</Button>
)}
<Button
leftIcon={<ExternalLinkIcon />}
onClick={() => payWithApp(promptInvoice)}
flex={1}
variant="solid"
size="md"
>
Open App
</Button>
</Flex>
</Flex>
) : (
<form onSubmit={handleSubmit(onSubmitZap)}>
<Flex gap="4" direction="column"> <Flex gap="4" direction="column">
<Flex gap="2" alignItems="center"> <Flex gap="2" alignItems="center">
<UserAvatar pubkey={pubkey} size="sm" /> <UserAvatar pubkey={pubkey} size="md" />
<Text>{actionName}</Text> <Box>
<UserLink pubkey={pubkey} /> <UserLink pubkey={pubkey} fontWeight="bold" />
<Text>{tipAddress}</Text>
</Box>
</Flex> </Flex>
<Flex gap="2" alignItems="center" flexWrap="wrap">
{customZapAmounts {stream && (
.split(",") <Box>
.map((v) => parseInt(v)) <Heading size="sm" mb="2">
.map((amount, i) => ( Stream: {stream.title}
<Button key={amount + i} onClick={() => setValue("amount", amount)} size="sm" variant="outline"> </Heading>
{amount} {stream.image && <Image src={stream.image} />}
</Button> </Box>
))}
</Flex>
<Flex gap="2">
<InputGroup maxWidth={32}>
{!isMobile && (
<InputLeftElement pointerEvents="none" color="gray.300" fontSize="1.2em">
<LightningIcon fontSize="1rem" color="yellow.400" />
</InputLeftElement>
)} )}
<Input {event && <EmbeddedNote note={event} />}
type="number"
placeholder="amount"
isInvalid={!!errors.amount}
step={1}
{...register("amount", { valueAsNumber: true, min: 1, required: true })}
/>
</InputGroup>
{(canZap || lnurlMetadata?.commentAllowed) && ( {(canZap || lnurlMetadata?.commentAllowed) && (
<Input <Input
placeholder="Comment" placeholder="Comment"
{...register("comment", { maxLength: lnurlMetadata?.commentAllowed ?? 150 })} {...register("comment", { maxLength: lnurlMetadata?.commentAllowed ?? 150 })}
autoComplete="off" autoComplete="off"
autoFocus={!initialComment}
/> />
)} )}
<Flex gap="2" alignItems="center" flexWrap="wrap">
{customZapAmounts
.split(",")
.map((v) => parseInt(v))
.map((amount, i) => (
<Button
key={amount + i}
onClick={() => {
setValue("amount", amount);
}}
leftIcon={<LightningIcon color="yellow.400" />}
variant="solid"
>
{amount}
</Button>
))}
</Flex> </Flex>
<Flex gap="2">
<Input
type="number"
placeholder="Custom amount"
isInvalid={!!errors.amount}
step={1}
flex={1}
{...register("amount", { valueAsNumber: true, min: 1 })}
/>
<Button leftIcon={<LightningIcon />} type="submit" isLoading={isSubmitting} variant="solid" size="md"> <Button leftIcon={<LightningIcon />} type="submit" isLoading={isSubmitting} variant="solid" size="md">
{actionName} {getUserDisplayName(metadata, pubkey)} {readablizeSats(watch("amount"))} sats {actionName} {readablizeSats(watch("amount"))} sats
</Button> </Button>
</Flex> </Flex>
</Flex>
</form> </form>
)}
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@ -61,7 +61,7 @@ export function parseStreamEvent(stream: NostrEvent): ParsedStream {
} }
export function getATag(stream: ParsedStream) { export function getATag(stream: ParsedStream) {
return `${stream.event.kind}:${stream.author}:${stream.starts}`; return `${stream.event.kind}:${stream.author}:${stream.identifier}`;
} }
export function buildChatMessage(stream: ParsedStream, content: string) { export function buildChatMessage(stream: ParsedStream, content: string) {

View File

@ -11,12 +11,6 @@ import {
Heading, Heading,
IconButton, IconButton,
Input, Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
Spacer, Spacer,
Text, Text,
useDisclosure, useDisclosure,
@ -30,7 +24,7 @@ import useSubject from "../../../hooks/use-subject";
import { truncatedId } from "../../../helpers/nostr-event"; import { truncatedId } from "../../../helpers/nostr-event";
import { UserAvatar } from "../../../components/user-avatar"; import { UserAvatar } from "../../../components/user-avatar";
import { UserLink } from "../../../components/user-link"; import { UserLink } from "../../../components/user-link";
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event"; import { 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 { EmbedableContent, embedUrls } from "../../../helpers/embeds"; import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
@ -50,14 +44,13 @@ import { useUserRelays } from "../../../hooks/use-user-relays";
import { RelayMode } from "../../../classes/relay"; import { RelayMode } from "../../../classes/relay";
import { unique } from "../../../helpers/array"; import { unique } from "../../../helpers/array";
import { LightningIcon } from "../../../components/icons"; import { LightningIcon } from "../../../components/icons";
import { parseZapEvent, requestZapInvoice } from "../../../helpers/zaps"; import { parseZapEvent } from "../../../helpers/zaps";
import { readablizeSats } from "../../../helpers/bolt11"; import { readablizeSats } from "../../../helpers/bolt11";
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 { ImageGalleryProvider } from "../../../components/image-gallery";
import appSettings from "../../../services/app-settings";
import { TrustProvider } from "../../../providers/trust"; import { TrustProvider } from "../../../providers/trust";
import ZapModal from "../../../components/zap-modal";
function ChatMessageContent({ event }: { event: NostrEvent }) { function ChatMessageContent({ event }: { event: NostrEvent }) {
const content = useMemo(() => { const content = useMemo(() => {
@ -133,11 +126,10 @@ 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);
const writeRelays = useUserRelays(stream.author) const userReadRelays = useUserRelays(stream.author)
.filter((r) => r.mode & RelayMode.READ) .filter((r) => r.mode & RelayMode.READ)
.map((r) => r.url); .map((r) => r.url);
@ -160,44 +152,16 @@ export default function StreamChat({
const draft = buildChatMessage(stream, values.content); const draft = buildChatMessage(stream, values.content);
const signed = await requestSignature(draft); const signed = await requestSignature(draft);
if (!signed) throw new Error("Failed to sign"); if (!signed) throw new Error("Failed to sign");
nostrPostAction(unique([...contextRelays, ...writeRelays]), signed); nostrPostAction(unique([...contextRelays, ...userReadRelays]), signed);
reset(); reset();
} catch (e) { } catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" }); if (e instanceof Error) toast({ description: e.message, status: "error" });
} }
}); });
const zapAmountModal = useDisclosure(); const zapModal = useDisclosure();
const { requestPay } = useInvoiceModalContext(); const { requestPay } = useInvoiceModalContext();
const zapMetadata = useUserLNURLMetadata(stream.author); const zapMetadata = useUserLNURLMetadata(stream.author);
const zapMessage = async (amount: number) => {
try {
if (!zapMetadata.metadata?.callback) throw new Error("bad lnurl endpoint");
const content = getValues().content;
const zapRequest: DraftNostrEvent = {
kind: Kind.ZapRequest,
created_at: dayjs().unix(),
content,
tags: [
["p", stream.author],
["a", getATag(stream)],
["relays", ...writeRelays],
["amount", String(amount * 1000)],
],
};
const signed = await requestSignature(zapRequest);
if (!signed) throw new Error("Failed to sign");
const invoice = await requestZapInvoice(signed, zapMetadata.metadata.callback);
await requestPay(invoice);
reset();
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
};
return ( return (
<> <>
@ -247,7 +211,7 @@ export default function StreamChat({
aria-label="Zap stream" aria-label="Zap stream"
borderColor="yellow.400" borderColor="yellow.400"
variant="outline" variant="outline"
onClick={zapAmountModal.onOpen} onClick={zapModal.onOpen}
/> />
)} )}
</Box> </Box>
@ -255,33 +219,20 @@ export default function StreamChat({
</Card> </Card>
</ImageGalleryProvider> </ImageGalleryProvider>
</IntersectionObserverProvider> </IntersectionObserverProvider>
<Modal isOpen={zapAmountModal.isOpen} onClose={zapAmountModal.onClose}> {zapModal.isOpen && (
<ModalOverlay /> <ZapModal
<ModalContent> isOpen
<ModalHeader pb="0">Zap Amount</ModalHeader> stream={stream}
<ModalCloseButton /> pubkey={stream.author}
<ModalBody> onInvoice={async (invoice) => {
<Flex gap="2" alignItems="center" flexWrap="wrap"> reset();
{customZapAmounts zapModal.onClose();
.split(",") await requestPay(invoice);
.map((v) => parseInt(v))
.map((amount, i) => (
<Button
key={amount + i}
onClick={() => {
zapAmountModal.onClose();
zapMessage(amount);
}} }}
size="sm" onClose={zapModal.onClose}
variant="outline" initialComment={getValues().content}
> />
{amount} )}
</Button>
))}
</Flex>
</ModalBody>
</ModalContent>
</Modal>
</> </>
); );
} }