diff --git a/src/Element/AsyncButton.tsx b/src/Element/AsyncButton.tsx index 0c12851d..784d9ea7 100644 --- a/src/Element/AsyncButton.tsx +++ b/src/Element/AsyncButton.tsx @@ -20,8 +20,8 @@ export default function AsyncButton(props: any) { } return ( -
handle(e)}> - {props.children} -
+ ) } \ No newline at end of file diff --git a/src/Element/Invoice.tsx b/src/Element/Invoice.tsx index 0bba9364..9fd867d2 100644 --- a/src/Element/Invoice.tsx +++ b/src/Element/Invoice.tsx @@ -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) { <>

Lightning Invoice

- setShowInvoice(false)} /> + setShowInvoice(false)} /> ) } diff --git a/src/Element/LNURLTip.css b/src/Element/LNURLTip.css deleted file mode 100644 index 20fb44ab..00000000 --- a/src/Element/LNURLTip.css +++ /dev/null @@ -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; -} diff --git a/src/Element/Nip5Service.tsx b/src/Element/Nip5Service.tsx index a7a23938..0d73597d 100644 --- a/src/Element/Nip5Service.tsx +++ b/src/Element/Nip5Service.tsx @@ -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 &&
Not available: {mapError(availabilityResponse.why!, availabilityResponse.reasonTag || null)}
} - setShowInvoice(false)} diff --git a/src/Element/NoteCreator.css b/src/Element/NoteCreator.css index ad0a7d62..98ac2ce4 100644 --- a/src/Element/NoteCreator.css +++ b/src/Element/NoteCreator.css @@ -12,6 +12,7 @@ } .note-creator textarea { + border: none; outline: none; resize: none; background-color: var(--note-bg); diff --git a/src/Element/NoteFooter.tsx b/src/Element/NoteFooter.tsx index e11be6ce..b41cf7db 100644 --- a/src/Element/NoteFooter.tsx +++ b/src/Element/NoteFooter.tsx @@ -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} /> - setTip(false)} show={tip} author={author?.pubkey} note={ev.Id} /> + setTip(false)} + show={tip} + author={author?.pubkey} + target={author?.display_name || author?.name} + note={ev.Id} + />
diff --git a/src/Element/SendSats.css b/src/Element/SendSats.css new file mode 100644 index 00000000..4e7e172f --- /dev/null +++ b/src/Element/SendSats.css @@ -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; +} diff --git a/src/Element/LNURLTip.tsx b/src/Element/SendSats.tsx similarity index 56% rename from src/Element/LNURLTip.tsx rename to src/Element/SendSats.tsx index e208e5b8..bf3807bc 100644 --- a/src/Element/LNURLTip.tsx +++ b/src/Element/SendSats.tsx @@ -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 = { + 1_000: "👍", + 5_000: "💜", + 10_000: "😍", + 20_000: "🤩", + 50_000: "🔥", + 100_000: "🚀", + 1_000_000: "🤯", + } const [payService, setPayService] = useState(); - const [amount, setAmount] = useState(); + const [amount, setAmount] = useState(500); const [customAmount, setCustomAmount] = useState(); const [invoice, setInvoice] = useState(); const [comment, setComment] = useState(); @@ -53,6 +70,7 @@ export default function LNURLTip(props: LNURLTipProps) { const [success, setSuccess] = useState(); 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 ( -
- setCustomAmount(parseInt(e.target.value))} /> -
selectAmount(customAmount!)}>Confirm
+
+ setCustomAmount(parseInt(e.target.value))} + /> +
); } @@ -182,22 +215,36 @@ export default function LNURLTip(props: LNURLTipProps) { if (invoice) return null; return ( <> -
{metadata?.description ?? service}
+

Zap amount in sats

+
+ {serviceAmounts.map(a => + selectAmount(a)}> + {emojis[a] && <>{emojis[a]}  } + {formatShort(a)} + + )} +
+ {payService && custom()}
- {(payService?.commentAllowed ?? 0) > 0 ? - setComment(e.target.value)} /> : null} + {(payService?.commentAllowed ?? 0) > 0 && + setComment(e.target.value)} + /> + }
-
- {serviceAmounts.map(a => selectAmount(a)}> - {a.toLocaleString()} - )} - {payService ? - selectAmount(-1)}> - Custom - : null} -
- {amount === -1 ? custom() : null} - {(amount ?? 0) > 0 && } + {(amount ?? 0) > 0 && ( + + )} ) } @@ -208,22 +255,20 @@ export default function LNURLTip(props: LNURLTipProps) { return ( <>
- {props.notice && {props.notice}} - -
- {pr && ( - <> -
- -
-
- -
- - )} -
+ {props.notice && {props.notice}} + +
+ {pr && ( + <> +
+ +
+ + + )} +
) @@ -232,24 +277,46 @@ export default function LNURLTip(props: LNURLTipProps) { function successAction() { if (!success) return null; return ( - <> -

{success?.description ?? "Paid!"}

- {success.url ? {success.url} : null} - +
+

+ + {success?.description ?? "Paid!"} +

+ {success.url && +

+ + {success.url} + +

+ } +
) } - 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 ( - -
e.stopPropagation()}> -

{props.title || defaultTitle}

- {invoiceForm()} - {error ?

{error}

: null} - {payInvoice()} - {successAction()} -
+ +
e.stopPropagation()}> +
+ +
+
+ {author && } +

+ {props.title || title} +

+
+ {invoiceForm()} + {error &&

{error}

} + {payInvoice()} + {successAction()} +
) } diff --git a/src/Element/Tabs.css b/src/Element/Tabs.css index 2b854d91..35b84ee0 100644 --- a/src/Element/Tabs.css +++ b/src/Element/Tabs.css @@ -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; } diff --git a/src/Element/Zap.css b/src/Element/Zap.css index 5de0871b..cb66101e 100644 --- a/src/Element/Zap.css +++ b/src/Element/Zap.css @@ -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 { diff --git a/src/Element/ZapButton.tsx b/src/Element/ZapButton.tsx index 4195e25a..b3022d8f 100644 --- a/src/Element/ZapButton.tsx +++ b/src/Element/ZapButton.tsx @@ -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 }) => {
setZap(true)}>
- setZap(false)} author={pubkey} /> + setZap(false)} author={pubkey} /> ) } diff --git a/src/Hooks/useHorizontalScroll.tsx b/src/Hooks/useHorizontalScroll.tsx new file mode 100644 index 00000000..88009ce0 --- /dev/null +++ b/src/Hooks/useHorizontalScroll.tsx @@ -0,0 +1,22 @@ +import { useEffect, useRef, WheelEvent, LegacyRef } from "react"; + +function useHorizontalScroll() { + const elRef = useRef(); + useEffect(() => { + const el = elRef.current; + if (el) { + const onWheel = (ev: WheelEvent) => { + if (ev.deltaY == 0) return; + ev.preventDefault(); + el.scrollTo({ left: el.scrollLeft + ev.deltaY, behavior: "smooth" }); + }; + // @ts-ignore + el.addEventListener("wheel", onWheel); + // @ts-ignore + return () => el.removeEventListener("wheel", onWheel); + } + }, []); + return elRef as LegacyRef | undefined +} + +export default useHorizontalScroll; diff --git a/src/Icons/Close.tsx b/src/Icons/Close.tsx new file mode 100644 index 00000000..0c038328 --- /dev/null +++ b/src/Icons/Close.tsx @@ -0,0 +1,11 @@ +import IconProps from "./IconProps"; + +const Close = (props: IconProps) => { + return ( + + + + ) +} + +export default Close diff --git a/src/Icons/Reply.tsx b/src/Icons/Reply.tsx index fa57e467..3d9cf0ba 100644 --- a/src/Icons/Reply.tsx +++ b/src/Icons/Reply.tsx @@ -1,8 +1,10 @@ +import IconProps from "./IconProps"; + const Reply = () => { return ( - - - + + + ) } diff --git a/src/Number.ts b/src/Number.ts index eb6c4aeb..a961556a 100644 --- a/src/Number.ts +++ b/src/Number.ts @@ -6,7 +6,7 @@ const intl = new Intl.NumberFormat("en", { export function formatShort(n: number) { if (n < 2e3) { return n - } else if (n < 1e8) { + } else if (n < 1e6) { return `${intl.format(n / 1e3)}K` } else { return `${intl.format(n / 1e6)}M` diff --git a/src/Pages/ProfilePage.css b/src/Pages/ProfilePage.css index 0666ea0f..3e88372d 100644 --- a/src/Pages/ProfilePage.css +++ b/src/Pages/ProfilePage.css @@ -62,7 +62,7 @@ white-space: pre-wrap; } -.profile .name h2 { +.details-wrapper > .name > h2 { margin: 12px 0 0 0; font-weight: 600; font-size: 19px; @@ -75,11 +75,11 @@ margin: 0 0 12px 0; } -.profile .avatar-wrapper { +.profile-wrapper > .avatar-wrapper { z-index: 1; } -.profile .avatar-wrapper .avatar { +.profile-wrapper > .avatar-wrapper .avatar { width: 120px; height: 120px; background-image: var(--img-url); diff --git a/src/Pages/ProfilePage.tsx b/src/Pages/ProfilePage.tsx index 2310c079..120fc74b 100644 --- a/src/Pages/ProfilePage.tsx +++ b/src/Pages/ProfilePage.tsx @@ -19,7 +19,7 @@ import Avatar from "Element/Avatar"; import LogoutButton from "Element/LogoutButton"; import Timeline from "Element/Timeline"; import Text from 'Element/Text' -import LNURLTip from "Element/LNURLTip"; +import SendSats from "Element/SendSats"; import Nip05 from "Element/Nip05"; import Copy from "Element/Copy"; import ProfilePreview from "Element/ProfilePreview"; @@ -35,6 +35,7 @@ import FollowsYou from "Element/FollowsYou" import QrCode from "Element/QrCode"; import Modal from "Element/Modal"; import { ProxyImg } from "Element/ProxyImg" +import useHorizontalScroll from "Hooks/useHorizontalScroll"; const ProfileTab = { Notes: { text: "Notes", value: 0 }, @@ -71,6 +72,7 @@ export default function ProfilePage() { return profileZaps }, [zapFeed.store, id]) const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0) + const horizontalScroll = useHorizontalScroll() useEffect(() => { setTab(ProfileTab.Notes); @@ -111,7 +113,13 @@ export default function ProfilePage() {
)} - setShowLnQr(false)} author={id} /> + setShowLnQr(false)} + author={id} + target={user?.display_name || user?.name} + />
) } @@ -242,7 +250,7 @@ export default function ProfilePage() { {userDetails()} -
+
{[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows, ProfileTab.Zaps, ProfileTab.Muted].map(renderTab)} {isMe && renderTab(ProfileTab.Blocked)}
diff --git a/src/index.css b/src/index.css index 692e33e6..925d7053 100644 --- a/src/index.css +++ b/src/index.css @@ -3,8 +3,8 @@ :root { --bg-color: #000; --font-color: #FFF; - --font-secondary-color: #7B7B7B; - --font-tertiary-color: #666; + --font-secondary-color: #A7A7A7; + --font-tertiary-color: #A3A3A3; --font-size: 16px; --font-size-small: 14px; --font-size-tiny: 12px; @@ -34,7 +34,7 @@ html.light { --bg-color: #F1F1F1; --font-color: #57534E; --font-secondary-color: #7B7B7B; - --font-tertiary-color: #F3F3F3; + --font-tertiary-color: #A7A7A7; --highlight-light: #16AAC1; --highlight: #0284C7; @@ -126,18 +126,19 @@ button { } button:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +button.secondary:disabled { + color: var(--font-secondary-color); cursor: not-allowed; - color: var(--font-color); - background-color: transparent; - border: 1px solid var(--font-secondary-color); } button:disabled:hover { cursor: not-allowed; color: var(--font-color); - background-color: transparent; - background-color: var(--gray-superdark); - border: 1px solid var(--font-secondary-color); + border-color: var(--gray-superdark); } .light button.transparent { @@ -239,14 +240,12 @@ button.icon:hover { } .btn-rnd { - border-radius: 100%; - border-color: var(--gray-secondary); + border: none; width: 21px; height: 21px; display: flex; align-items: center; justify-content: center; - margin-right: 16px; } @media (min-width: 520px) { @@ -257,12 +256,21 @@ textarea { font: inherit; } -input[type="text"], input[type="password"], input[type="number"], textarea, select { - padding: 10px; - border-radius: 5px; - border: 0; - background-color: var(--gray); +input[type="text"], input[type="password"], input[type="number"], select, textarea { + padding: 12px; color: var(--font-color); + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 16px; + outline: none; +} + +.light input[type="text"], .light input[type="password"], .light input[type="number"], .light select, .light textarea { + border: 1px solid rgba(0, 0, 0, 0.3); +} + +input:placeholder, textarea:placeholder { + color: var(--font-tertiary-color); } input[type="checkbox"] { @@ -275,10 +283,6 @@ input:disabled { cursor: not-allowed; } -textarea:placeholder { - color: var(--gray-superlight); -} - .flex { display: flex; align-items: center; @@ -451,6 +455,10 @@ body.scroll-lock { background-color: var(--error); } +.success { + color: var(--success); +} + .bg-success { background-color: var(--success); }