Zap modal (#209)

This commit is contained in:
Alejandro
2023-02-07 14:32:32 +01:00
committed by GitHub
parent 41aa93a279
commit 1e76e729f7
18 changed files with 403 additions and 159 deletions

View File

@ -20,8 +20,8 @@ export default function AsyncButton(props: any) {
}
return (
<div {...props} className={`btn ${props.className}${loading ? " disabled" : ""}`} onClick={(e) => handle(e)}>
{props.children}
</div>
<button type="button" disabled={loading} {...props} onClick={(e) => handle(e)}>
{props.children}
</button>
)
}

View File

@ -4,7 +4,7 @@ import { useState } from "react";
import { decode as invoiceDecode } from "light-bolt11-decoder";
import { useMemo } from "react";
import NoteTime from "Element/NoteTime";
import LNURLTip from "Element/LNURLTip";
import SendSats from "Element/SendSats";
import ZapCircle from "Icons/ZapCircle";
import useWebln from "Hooks/useWebln";
@ -49,7 +49,7 @@ export default function Invoice(props: InvoiceProps) {
<>
<h4>Lightning Invoice</h4>
<ZapCircle className="zap-circle" />
<LNURLTip invoice={invoice} show={showInvoice} onClose={() => setShowInvoice(false)} />
<SendSats title="Pay Invoice" invoice={invoice} show={showInvoice} onClose={() => setShowInvoice(false)} />
</>
)
}

View File

@ -1,59 +0,0 @@
.lnurl-tip {
text-align: center;
}
.lnurl-tip .btn {
background-color: inherit;
width: 210px;
margin: 0 0 10px 0;
}
.lnurl-tip .btn:hover {
background-color: var(--gray);
}
.sat-amount {
display: inline-block;
background-color: var(--gray-secondary);
color: var(--font-color);
padding: 2px 10px;
border-radius: 10px;
user-select: none;
margin: 2px 5px;
}
.sat-amount:hover {
cursor: pointer;
}
.sat-amount.active {
font-weight: bold;
color: var(--note-bg);
background-color: var(--font-color);
}
.lnurl-tip .invoice {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.lnurl-tip .invoice .actions {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: center;
}
.lnurl-tip .invoice .actions .copy-action {
margin: 10px auto;
}
.lnurl-tip .invoice .actions .pay-actions {
margin: 10px auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

View File

@ -11,7 +11,7 @@ import {
CheckRegisterResponse
} from "Nip05/ServiceProvider";
import AsyncButton from "Element/AsyncButton";
import LNURLTip from "Element/LNURLTip";
import SendSats from "Element/SendSats";
import Copy from "Element/Copy";
import { useUserProfile }from "Feed/ProfileFeed";
import useEventPublisher from "Feed/EventPublisher";
@ -176,7 +176,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
{availabilityResponse?.available === false && !registerStatus && <div className="flex">
<b className="error">Not available: {mapError(availabilityResponse.why!, availabilityResponse.reasonTag || null)}</b>
</div>}
<LNURLTip
<SendSats
invoice={registerResponse?.invoice}
show={showInvoice}
onClose={() => setShowInvoice(false)}

View File

@ -12,6 +12,7 @@
}
.note-creator textarea {
border: none;
outline: none;
resize: none;
background-color: var(--note-bg);

View File

@ -13,7 +13,7 @@ import { formatShort } from "Number";
import useEventPublisher from "Feed/EventPublisher";
import { getReactions, hexToBech32, normalizeReaction, Reaction } from "Util";
import { NoteCreator } from "Element/NoteCreator";
import LNURLTip from "Element/LNURLTip";
import SendSats from "Element/SendSats";
import { parseZap, ZapsSummary } from "Element/Zap";
import { useUserProfile } from "Feed/ProfileFeed";
import { default as NEvent } from "Nostr/Event";
@ -268,7 +268,14 @@ export default function NoteFooter(props: NoteFooterProps) {
show={reply}
setShow={setReply}
/>
<LNURLTip svc={author?.lud16 || author?.lud06} onClose={() => setTip(false)} show={tip} author={author?.pubkey} note={ev.Id} />
<SendSats
svc={author?.lud16 || author?.lud06}
onClose={() => setTip(false)}
show={tip}
author={author?.pubkey}
target={author?.display_name || author?.name}
note={ev.Id}
/>
</div>
<div className="zaps-container">
<ZapsSummary zaps={zaps} />

176
src/Element/SendSats.css Normal file
View File

@ -0,0 +1,176 @@
.lnurl-modal .modal-body {
padding: 0;
max-width: 470px;
}
.lnurl-modal .lnurl-tip .pfp .avatar {
width: 48px;
height: 48px;
}
.lnurl-tip {
padding: 24px 32px;
background-color: #1B1B1B;
border-radius: 16px;
position: relative;
}
@media (max-width: 720px) {
.lnurl-tip {
padding: 12px 16px;
}
}
.light .lnurl-tip {
background-color: var(--note-bg);
}
.lnurl-tip h3 {
color: var(--font-secondary-color);
font-size: 11px;
letter-spacing: .11em;
font-weight: 600;
line-height: 13px;
text-transform: uppercase;
}
.lnurl-tip .close {
position: absolute;
top: 12px;
right: 16px;
color: var(--font-secondary-color);
cursor: pointer;
}
.lnurl-tip .close:hover {
color: var(--font-tertiary-color);
}
.lnurl-tip .lnurl-header {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 32px;
}
.lnurl-tip .lnurl-header h2 {
margin: 0;
flex-grow: 1;
font-weight: 600;
font-size: 16px;
line-height: 19px;
}
.lnurl-tip .btn {
background-color: inherit;
width: 210px;
margin: 0 0 10px 0;
}
.lnurl-tip .btn:hover {
}
.amounts {
display: flex;
width: 100%;
overflow-x: scroll;
-ms-overflow-style: none; /* for Internet Explorer, Edge */
scrollbar-width: none; /* for Firefox */
margin-bottom: 16px;
}
.amounts::-webkit-scrollbar {
display: none;
}
.sat-amount {
text-align: center;
display: inline-block;
background-color: #2A2A2A;
color: var(--font-color);
padding: 12px 16px;
border-radius: 100px;
user-select: none;
font-weight: 600;
font-size: 14px;
line-height: 17px;
}
.light .sat-amount {
background-color: var(--gray);
}
.sat-amount:not(:last-child) {
margin-right: 8px;
}
.sat-amount:hover {
cursor: pointer;
}
.sat-amount.active {
font-weight: bold;
color: var(--note-bg);
background-color: var(--font-color);
}
.lnurl-tip .invoice {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.lnurl-tip .invoice .actions {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: center;
}
.lnurl-tip .invoice .actions .copy-action {
margin: 10px auto;
}
.lnurl-tip .invoice .actions .wallet-action {
width: 100%;
height: 40px;
}
.lnurl-tip .zap-action {
margin-top: 16px;
width: 100%;
height: 40px;
}
.lnurl-tip .zap-action svg {
margin-right: 10px;
}
.lnurl-tip .zap-action-container {
display: flex;
align-items: center;
justify-content: center;
}
.lnurl-tip .custom-amount {
margin-bottom: 16px;
}
.lnurl-tip .custom-amount button {
padding: 12px 18px;
border-radius: 100px;
}
.lnurl-tip canvas {
border-radius: 10px;
}
.lnurl-tip .success-action .paid {
font-size: 19px;
}
.lnurl-tip .success-action a {
color: var(--highlight);
font-size: 19px;
}

View File

@ -1,12 +1,19 @@
import "./LNURLTip.css";
import "./SendSats.css";
import { useEffect, useMemo, useState } from "react";
import { formatShort } from "Number";
import { bech32ToText } from "Util";
import { HexKey } from "Nostr";
import Check from "Icons/Check";
import Zap from "Icons/Zap";
import Close from "Icons/Close";
import useEventPublisher from "Feed/EventPublisher";
import ProfileImage from "Element/ProfileImage";
import Modal from "Element/Modal";
import QrCode from "Element/QrCode";
import Copy from "Element/Copy";
import useWebln from "Hooks/useWebln";
import useHorizontalScroll from "Hooks/useHorizontalScroll";
interface LNURLService {
nostrPubkey?: HexKey
@ -34,6 +41,7 @@ export interface LNURLTipProps {
invoice?: string, // shortcut to invoice qr tab
title?: string,
notice?: string
target?: string
note?: HexKey
author?: HexKey
}
@ -42,10 +50,19 @@ export default function LNURLTip(props: LNURLTipProps) {
const onClose = props.onClose || (() => { });
const service = props.svc;
const show = props.show || false;
const { note, author } = props
const amounts = [50, 100, 500, 1_000, 5_000, 10_000, 50_000];
const { note, author, target } = props
const amounts = [500, 1_000, 5_000, 10_000, 20_000, 50_000, 100_000, 1_000_000];
const emojis: Record<number, string> = {
1_000: "👍",
5_000: "💜",
10_000: "😍",
20_000: "🤩",
50_000: "🔥",
100_000: "🚀",
1_000_000: "🤯",
}
const [payService, setPayService] = useState<LNURLService>();
const [amount, setAmount] = useState<number>();
const [amount, setAmount] = useState<number>(500);
const [customAmount, setCustomAmount] = useState<number>();
const [invoice, setInvoice] = useState<LNURLInvoice>();
const [comment, setComment] = useState<string>();
@ -53,6 +70,7 @@ export default function LNURLTip(props: LNURLTipProps) {
const [success, setSuccess] = useState<LNURLSuccessAction>();
const webln = useWebln(show);
const publisher = useEventPublisher();
const horizontalScroll = useHorizontalScroll();
useEffect(() => {
if (show && !props.invoice) {
@ -63,7 +81,7 @@ export default function LNURLTip(props: LNURLTipProps) {
setPayService(undefined);
setError(undefined);
setInvoice(props.invoice ? { pr: props.invoice } : undefined);
setAmount(undefined);
setAmount(500);
setComment(undefined);
setSuccess(undefined);
}
@ -155,12 +173,27 @@ export default function LNURLTip(props: LNURLTipProps) {
};
function custom() {
let min = (payService?.minSendable ?? 0) / 1000;
let min = (payService?.minSendable ?? 1000) / 1000;
let max = (payService?.maxSendable ?? 21_000_000_000) / 1000;
return (
<div className="flex mb10">
<input type="number" min={min} max={max} className="f-grow mr10" value={customAmount} onChange={(e) => setCustomAmount(parseInt(e.target.value))} />
<div className="btn" onClick={() => selectAmount(customAmount!)}>Confirm</div>
<div className="custom-amount flex">
<input
type="number"
min={min}
max={max}
className="f-grow mr10"
placeholder="Custom"
value={customAmount}
onChange={(e) => setCustomAmount(parseInt(e.target.value))}
/>
<button
className="secondary"
type="button"
disabled={!Boolean(customAmount)}
onClick={() => selectAmount(customAmount!)}
>
Confirm
</button>
</div>
);
}
@ -182,22 +215,36 @@ export default function LNURLTip(props: LNURLTipProps) {
if (invoice) return null;
return (
<>
<div className="f-ellipsis mb10">{metadata?.description ?? service}</div>
<h3>Zap amount in sats</h3>
<div className="amounts" ref={horizontalScroll}>
{serviceAmounts.map(a =>
<span className={`sat-amount ${amount === a ? "active" : ""}`} key={a} onClick={() => selectAmount(a)}>
{emojis[a] && <>{emojis[a]}&nbsp;</> }
{formatShort(a)}
</span>
)}
</div>
{payService && custom()}
<div className="flex">
{(payService?.commentAllowed ?? 0) > 0 ?
<input type="text" placeholder="Comment" className="mb10 f-grow" maxLength={payService?.commentAllowed} onChange={(e) => setComment(e.target.value)} /> : null}
{(payService?.commentAllowed ?? 0) > 0 &&
<input
type="text"
placeholder="Comment"
className="f-grow"
maxLength={payService?.commentAllowed}
onChange={(e) => setComment(e.target.value)}
/>
}
</div>
<div className="mb10">
{serviceAmounts.map(a => <span className={`sat-amount ${amount === a ? "active" : ""}`} key={a} onClick={() => selectAmount(a)}>
{a.toLocaleString()}
</span>)}
{payService ?
<span className={`sat-amount ${amount === -1 ? "active" : ""}`} onClick={() => selectAmount(-1)}>
Custom
</span> : null}
</div>
{amount === -1 ? custom() : null}
{(amount ?? 0) > 0 && <button type="button" className="mb10" onClick={() => loadInvoice()}>Get Invoice</button>}
{(amount ?? 0) > 0 && (
<button type="button" className="zap-action" onClick={() => loadInvoice()}>
<div className="zap-action-container">
<Zap /> Zap
{target && ` ${target} `}
{formatShort(amount)} sats
</div>
</button>
)}
</>
)
}
@ -208,22 +255,20 @@ export default function LNURLTip(props: LNURLTipProps) {
return (
<>
<div className="invoice">
{props.notice && <b className="error">{props.notice}</b>}
<QrCode data={pr} link={`lightning:${pr}`} />
<div className="actions">
{pr && (
<>
<div className="copy-action">
<Copy text={pr} maxSize={26} />
</div>
<div className="pay-actions">
<button type="button" onClick={() => window.open(`lightning:${pr}`)}>
Open Wallet
</button>
</div>
</>
)}
</div>
{props.notice && <b className="error">{props.notice}</b>}
<QrCode data={pr} link={`lightning:${pr}`} />
<div className="actions">
{pr && (
<>
<div className="copy-action">
<Copy text={pr} maxSize={26} />
</div>
<button className="wallet-action" type="button" onClick={() => window.open(`lightning:${pr}`)}>
Open Wallet
</button>
</>
)}
</div>
</div>
</>
)
@ -232,24 +277,46 @@ export default function LNURLTip(props: LNURLTipProps) {
function successAction() {
if (!success) return null;
return (
<>
<p>{success?.description ?? "Paid!"}</p>
{success.url ? <a href={success.url} rel="noreferrer" target="_blank">{success.url}</a> : null}
</>
<div className="success-action">
<p className="paid">
<Check className="success mr10" />
{success?.description ?? "Paid!"}
</p>
{success.url &&
<p>
<a
href={success.url}
rel="noreferrer"
target="_blank"
>
{success.url}
</a>
</p>
}
</div>
)
}
const defaultTitle = payService?.nostrPubkey ? "⚡️ Send Zap!" : "⚡️ Send sats";
const defaultTitle = payService?.nostrPubkey ? "Send zap" : "Send sats";
const title = target ? `${defaultTitle} to ${target}` : defaultTitle
if (!show) return null;
return (
<Modal onClose={onClose}>
<div className="lnurl-tip" onClick={(e) => e.stopPropagation()}>
<h2>{props.title || defaultTitle}</h2>
{invoiceForm()}
{error ? <p className="error">{error}</p> : null}
{payInvoice()}
{successAction()}
</div>
<Modal className="lnurl-modal" onClose={onClose}>
<div className="lnurl-tip" onClick={(e) => e.stopPropagation()}>
<div className="close" onClick={onClose}>
<Close />
</div>
<div className="lnurl-header">
{author && <ProfileImage pubkey={author} showUsername={false} />}
<h2>
{props.title || title}
</h2>
</div>
{invoiceForm()}
{error && <p className="error">{error}</p>}
{payInvoice()}
{successAction()}
</div>
</Modal>
)
}

View File

@ -2,7 +2,8 @@
display: flex;
align-items: center;
flex-direction: row;
overflow-x: auto;
overflow-x: scroll;
-ms-overflow-style: none; /* for Internet Explorer, Edge */
scrollbar-width: none; /* Firefox */
margin-bottom: 18px;
}
@ -12,7 +13,8 @@
}
.tab {
border: 1px solid var(--gray-secondary);
color: var(--font-tertiary-color);
border: 1px solid var(--font-tertiary-color);
border-radius: 16px;
text-align: center;
font-weight: 600;
@ -21,7 +23,6 @@
font-weight: 600;
font-size: 14px;
line-height: 17px;
color: #A3A3A3;
margin-right: 12px;
}

View File

@ -52,7 +52,7 @@
margin-top: 8px;
display: flex;
flex-direction: row;
margin-left: 52px;
margin-left: 56px;
}
.note.thread-root .zaps-summary {
@ -70,8 +70,8 @@
}
.top-zap .avatar {
width: 21px;
height: 21px;
width: 18px;
height: 18px;
}
.top-zap .nip05 {

View File

@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react";
import { useUserProfile } from "Feed/ProfileFeed";
import { HexKey } from "Nostr";
import LNURLTip from "Element/LNURLTip";
import SendSats from "Element/SendSats";
const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey, svc?: string }) => {
@ -19,7 +19,7 @@ const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey, svc?: string }) => {
<div className="zap-button" onClick={(e) => setZap(true)}>
<FontAwesomeIcon icon={faBolt} />
</div>
<LNURLTip svc={service} show={zap} onClose={() => setZap(false)} author={pubkey} />
<SendSats target={profile?.display_name || profile?.name} svc={service} show={zap} onClose={() => setZap(false)} author={pubkey} />
</>
)
}