feat: add instant zap

This commit is contained in:
2024-01-18 13:56:35 +07:00
parent 240fe8bc7c
commit 0e9418949b
15 changed files with 809 additions and 341 deletions

View File

@@ -1,16 +1,11 @@
import { webln } from "@getalby/sdk";
import { SendPaymentResponse } from "@getalby/sdk/dist/types";
import { CancelIcon, ZapIcon } from "@lume/icons";
import {
compactNumber,
displayNpub,
sendNativeNotification,
} from "@lume/utils";
import { type SendPaymentResponse } from "@getalby/sdk/dist/types";
import { CancelIcon, LoaderIcon, ZapIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { cn, compactNumber, displayNpub } from "@lume/utils";
import * as Dialog from "@radix-ui/react-dialog";
import * as Tooltip from "@radix-ui/react-tooltip";
import { invoke } from "@tauri-apps/api/core";
import { QRCodeSVG } from "qrcode.react";
import { useEffect, useState } from "react";
import { useState } from "react";
import CurrencyInput from "react-currency-input-field";
import { toast } from "sonner";
import { useArk } from "../../../hooks/useArk";
@@ -18,83 +13,98 @@ import { useProfile } from "../../../hooks/useProfile";
import { useNoteContext } from "../provider";
export function NoteZap() {
const [walletConnectURL, setWalletConnectURL] = useState<string>(null);
const ark = useArk();
const storage = useStorage();
const event = useNoteContext();
const [amount, setAmount] = useState<string>("21");
const [zapMessage, setZapMessage] = useState<string>("");
const [invoice, setInvoice] = useState<null | string>(null);
const [isOpen, setIsOpen] = useState(false);
const [isCompleted, setIsCompleted] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const ark = useArk();
const event = useNoteContext();
const { user } = useProfile(event.pubkey);
const createZapRequest = async () => {
const createZapRequest = async (instant?: boolean) => {
if (!storage.nwc) return;
let nwc: webln.NostrWebLNProvider = undefined;
try {
// start loading
setIsLoading(true);
const zapAmount = parseInt(amount) * 1000;
const res = await event.zap(zapAmount, zapMessage);
if (!res) return toast.error("Cannot create zap request");
// user don't connect nwc, create QR Code for invoice
if (!walletConnectURL) return setInvoice(res);
// user connect nwc
nwc = new webln.NostrWebLNProvider({
nostrWalletConnectUrl: walletConnectURL,
nostrWalletConnectUrl: storage.nwc,
});
await nwc.enable();
// start loading
setIsLoading(true);
// send payment via nwc
const send: SendPaymentResponse = await nwc.sendPayment(res);
if (send) {
await sendNativeNotification(
toast.success(
`You've zapped ${compactNumber.format(send.amount)} sats to ${
user?.name || user?.displayName || "anon"
}`,
);
// eose
nwc.close();
setIsCompleted(true);
setIsLoading(false);
// reset after 1.5 secs
const timeout = setTimeout(() => setIsCompleted(false), 1500);
clearTimeout(timeout);
if (!instant) {
const timeout = setTimeout(() => setIsCompleted(false), 1500);
clearTimeout(timeout);
}
}
} catch (e) {
// eose
nwc.close();
// update state
setIsCompleted(true);
setIsLoading(false);
} catch (e) {
nwc?.close();
setIsLoading(false);
toast.error(String(e));
}
};
useEffect(() => {
async function getWalletConnectURL() {
const uri: string = await invoke("secure_load", {
key: `${ark.account.pubkey}-nwc`,
});
if (uri) setWalletConnectURL(uri);
}
if (isOpen) getWalletConnectURL();
return () => {
setAmount("21");
setZapMessage("");
setIsCompleted(false);
setIsLoading(false);
};
}, [isOpen]);
if (storage.settings.instantZap) {
return (
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => createZapRequest(true)}
className="inline-flex items-center justify-center group size-7 text-neutral-600 dark:text-neutral-400"
>
{isLoading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
<ZapIcon
className={cn(
"size-5 group-hover:text-blue-500",
isCompleted ? "text-blue-500" : "",
)}
/>
)}
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Zap
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
return (
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
@@ -135,129 +145,99 @@ export function NoteZap() {
</Dialog.Close>
</div>
<div className="px-5 pb-5 overflow-x-hidden overflow-y-auto">
{!invoice ? (
<>
<div className="relative flex flex-col h-40">
<div className="inline-flex items-center justify-center flex-1 h-full gap-1">
<CurrencyInput
placeholder="0"
defaultValue={"21"}
value={amount}
decimalsLimit={2}
min={0} // 0 sats
max={10000} // 1M sats
maxLength={10000} // 1M sats
onValueChange={(value) => setAmount(value)}
className="flex-1 w-full text-4xl font-semibold text-right bg-transparent border-none placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
/>
<span className="flex-1 w-full text-4xl font-semibold text-left text-neutral-500 dark:text-neutral-400">
sats
</span>
</div>
<div className="inline-flex items-center justify-center gap-2">
<button
type="button"
onClick={() => setAmount("69")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
69 sats
</button>
<button
type="button"
onClick={() => setAmount("100")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
100 sats
</button>
<button
type="button"
onClick={() => setAmount("200")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
200 sats
</button>
<button
type="button"
onClick={() => setAmount("500")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
500 sats
</button>
<button
type="button"
onClick={() => setAmount("1000")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
1K sats
</button>
</div>
</div>
<div className="flex flex-col w-full gap-2 mt-4">
<input
name="zapMessage"
value={zapMessage}
onChange={(e) => setZapMessage(e.target.value)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="Enter message (optional)"
className="w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-400"
/>
<div className="flex flex-col gap-2">
{walletConnectURL ? (
<button
type="button"
onClick={() => createZapRequest()}
className="inline-flex items-center justify-center w-full px-4 font-medium text-white bg-blue-500 rounded-lg h-11 hover:bg-blue-600"
>
{isCompleted ? (
<p className="leading-tight">Successfully zapped</p>
) : isLoading ? (
<span className="flex flex-col">
<p className="leading-tight">
Waiting for approval
</p>
<p className="text-xs leading-tight text-neutral-100">
Go to your wallet and approve payment request
</p>
</span>
) : (
<span className="flex flex-col">
<p className="leading-tight">Send zap</p>
<p className="text-xs leading-tight text-neutral-100">
You&apos;re using nostr wallet connect
</p>
</span>
)}
</button>
) : (
<button
type="button"
onClick={() => createZapRequest()}
className="inline-flex items-center justify-center w-full px-4 font-medium text-white bg-blue-500 rounded-lg h-11 hover:bg-blue-600"
>
Create Lightning invoice
</button>
)}
</div>
</div>
</>
) : (
<div className="flex flex-col items-center justify-center gap-4 mt-3">
<div className="p-3 rounded-md bg-neutral-100 dark:bg-neutral-900">
<QRCodeSVG value={invoice} size={256} />
</div>
<div className="flex flex-col items-center gap-1">
<h3 className="text-lg font-medium">Scan to zap</h3>
<span className="text-sm text-center text-neutral-600 dark:text-neutral-400">
You must use Bitcoin wallet which support Lightning
<br />
such as: Blue Wallet, Bitkit, Phoenix,...
</span>
</div>
<div className="relative flex flex-col h-40">
<div className="inline-flex items-center justify-center flex-1 h-full gap-1">
<CurrencyInput
placeholder="0"
defaultValue={"21"}
value={amount}
decimalsLimit={2}
min={0} // 0 sats
max={10000} // 1M sats
maxLength={10000} // 1M sats
onValueChange={(value) => setAmount(value)}
className="flex-1 w-full text-4xl font-semibold text-right bg-transparent border-none placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
/>
<span className="flex-1 w-full text-4xl font-semibold text-left text-neutral-500 dark:text-neutral-400">
sats
</span>
</div>
)}
<div className="inline-flex items-center justify-center gap-2">
<button
type="button"
onClick={() => setAmount("69")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
69 sats
</button>
<button
type="button"
onClick={() => setAmount("100")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
100 sats
</button>
<button
type="button"
onClick={() => setAmount("200")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
200 sats
</button>
<button
type="button"
onClick={() => setAmount("500")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
500 sats
</button>
<button
type="button"
onClick={() => setAmount("1000")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
1K sats
</button>
</div>
</div>
<div className="flex flex-col w-full gap-2 mt-4">
<input
name="zapMessage"
value={zapMessage}
onChange={(e) => setZapMessage(e.target.value)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="Enter message (optional)"
className="w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-400"
/>
<div className="flex flex-col gap-2">
<button
type="button"
onClick={() => createZapRequest()}
className="inline-flex items-center justify-center w-full px-4 font-medium text-white bg-blue-500 rounded-lg h-11 hover:bg-blue-600"
>
{isCompleted ? (
<p className="leading-tight">Successfully zapped</p>
) : isLoading ? (
<span className="flex flex-col">
<p className="leading-tight">Waiting for approval</p>
<p className="text-xs leading-tight text-neutral-100">
Go to your wallet and approve payment request
</p>
</span>
) : (
<span className="flex flex-col">
<p className="leading-tight">Send zap</p>
<p className="text-xs leading-tight text-neutral-100">
You&apos;re using nostr wallet connect
</p>
</span>
)}
</button>
</div>
</div>
</div>
</div>
</Dialog.Content>

View File

@@ -38,7 +38,7 @@ export function NoteContent({
const [content, setContent] = useState(event.content);
const [translate, setTranslate] = useState({
translatable: true,
translatable: false,
translated: false,
});

View File

@@ -42,7 +42,7 @@ export function LinkPreview({ url }: { url: string }) {
to={url}
target="_blank"
rel="noreferrer"
className="flex flex-col w-full my-1 overflow-hidden rounded-lg bg-neutral-100 dark:bg-neutral-900"
className="flex flex-col w-full my-1 overflow-hidden rounded-lg bg-neutral-100 dark:bg-neutral-900 border border-black/5 dark:border-white/5"
>
{isImage(data.image) ? (
<img

View File

@@ -12,7 +12,13 @@ export function VideoPreview({ url }: { url: string }) {
return (
<div className="my-1 w-full rounded-lg overflow-hidden">
<MediaController>
<video slot="media" src={url} preload="auto" muted />
<video
slot="media"
src={url}
preload="auto"
muted
className="w-full h-auto"
/>
<MediaControlBar>
<MediaPlayButton />
<MediaTimeRange />

View File

@@ -6,13 +6,13 @@ export function ChildReply({
}: { event: NDKEvent; rootEventId?: string }) {
return (
<Note.Provider event={event}>
<Note.Root className="pl-6">
<Note.Root className="py-2">
<div className="flex items-center justify-between h-14">
<Note.User className="flex-1" />
<Note.Menu />
</div>
<Note.Content />
<div className="flex items-center justify-end gap-10 mt-4">
<div className="flex items-center justify-end gap-4 mt-2">
<Note.Repost />
<Note.Zap />
</div>

View File

@@ -37,14 +37,20 @@ export function Reply({
) : (
<div />
)}
<div className="inline-flex items-center gap-10">
<div className="inline-flex items-center gap-4">
<Note.Repost />
<Note.Zap />
</div>
</div>
<div className={cn("", open ? "pb-3" : "")}>
<div
className={cn(
open
? "pb-3 border-t border-neutral-100 dark:border-neutral-900"
: "",
)}
>
{event.replies?.length > 0 ? (
<Collapsible.Content>
<Collapsible.Content className="divide-y divide-neutral-100 dark:divide-neutral-900 pl-6">
{event.replies?.map((childEvent) => (
<ChildReply key={childEvent.id} event={childEvent} />
))}

View File

@@ -32,7 +32,7 @@ export function ThreadNote({ eventId }: { eventId: string }) {
<Note.Content className="min-w-0 px-3" />
<div className="flex items-center justify-between px-3 h-14">
<Note.Pin />
<div className="inline-flex items-center gap-10">
<div className="inline-flex items-center gap-4">
<Note.Repost />
<Note.Zap />
</div>