feat: fast zaps

This commit is contained in:
Kieran 2023-02-25 21:18:36 +00:00
parent f934dcd092
commit 8c286c04f3
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
14 changed files with 355 additions and 138 deletions

View File

@ -11,6 +11,7 @@
"@noble/secp256k1": "^1.7.0",
"@protobufjs/base64": "^1.1.2",
"@reduxjs/toolkit": "^1.9.1",
"@snort/nostr": "^1.0.0",
"@szhsin/react-menu": "^3.3.1",
"@types/jest": "^29.2.5",
"@types/node": "^18.11.18",
@ -37,6 +38,7 @@
"react-twitter-embed": "^4.0.4",
"typescript": "^4.9.4",
"unist-util-visit": "^4.1.2",
"use-long-press": "^2.0.3",
"uuid": "^9.0.0",
"workbox-background-sync": "^6.4.2",
"workbox-broadcast-update": "^6.4.2",
@ -49,8 +51,7 @@
"workbox-range-requests": "^6.4.2",
"workbox-routing": "^6.4.2",
"workbox-strategies": "^6.4.2",
"workbox-streams": "^6.4.2",
"@snort/nostr": "^1.0.0"
"workbox-streams": "^6.4.2"
},
"scripts": {
"start": "react-app-rewired start",

View File

@ -1,7 +1,8 @@
import { useState } from "react";
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useIntl, FormattedMessage } from "react-intl";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { useLongPress } from "use-long-press";
import Bookmark from "Icons/Bookmark";
import Pin from "Icons/Pin";
@ -31,6 +32,10 @@ import { RootState } from "State/Store";
import { UserPreferences, setPinned, setBookmarked } from "State/Login";
import useModeration from "Hooks/useModeration";
import { TranslateHost } from "Const";
import useWebln from "Hooks/useWebln";
import { LNURL } from "LNURL";
import Spinner from "Icons/Spinner";
import ZapFast from "Icons/ZapFast";
import messages from "./messages";
@ -63,6 +68,8 @@ export default function NoteFooter(props: NoteFooterProps) {
const publisher = useEventPublisher();
const [reply, setReply] = useState(false);
const [tip, setTip] = useState(false);
const [zapping, setZapping] = useState(false);
const webln = useWebln();
const isMine = ev.RootPubKey === login;
const lang = window.navigator.language;
const langNames = new Intl.DisplayNames([...window.navigator.languages], {
@ -70,6 +77,15 @@ export default function NoteFooter(props: NoteFooterProps) {
});
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
const didZap = zaps.some(a => a.zapper === login);
const longPress = useLongPress(
e => {
e.stopPropagation();
setTip(true);
},
{
captureEvent: true,
}
);
function hasReacted(emoji: string) {
return positive?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === login);
@ -102,15 +118,39 @@ export default function NoteFooter(props: NoteFooterProps) {
}
}
async function zapClick(e: React.MouseEvent) {
if (zapping || e.isPropagationStopped()) return;
const lnurl = author?.lud16 || author?.lud06;
if (webln?.enabled && lnurl) {
setZapping(true);
try {
const handler = new LNURL(lnurl);
await handler.load();
const zap = await publisher.zap(prefs.defaultZapAmount * 1000, ev.PubKey, ev.Id);
const invoice = await handler.getInvoice(prefs.defaultZapAmount, undefined, zap);
if (invoice.pr) {
await webln.sendPayment(invoice.pr);
}
} catch (e) {
console.warn("Instant zap failed", e);
setTip(true);
} finally {
setZapping(false);
}
} else {
setTip(true);
}
}
function tipButton() {
const service = author?.lud16 || author?.lud06;
if (service) {
return (
<>
<div className={`reaction-pill ${didZap ? "reacted" : ""}`} onClick={() => setTip(true)}>
<div className="reaction-pill-icon">
<Zap />
</div>
<div className={`reaction-pill ${didZap ? "reacted" : ""}`} {...longPress()} onClick={e => zapClick(e)}>
<div className="reaction-pill-icon">{zapping ? <Spinner /> : webln?.enabled ? <ZapFast /> : <Zap />}</div>
{zapTotal > 0 && <div className="reaction-pill-number">{formatShort(zapTotal)}</div>}
</div>
</>
@ -309,7 +349,7 @@ export default function NoteFooter(props: NoteFooterProps) {
zaps={zaps}
/>
<SendSats
svc={author?.lud16 || author?.lud06}
lnurl={author?.lud16 || author?.lud06}
onClose={() => setTip(false)}
show={tip}
author={author?.pubkey}

View File

@ -1,11 +1,10 @@
import "./SendSats.css";
import { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useSelector } from "react-redux";
import { formatShort } from "Number";
import { bech32ToText } from "Util";
import { HexKey, Tag } from "@snort/nostr";
import { Event, HexKey, Tag } from "@snort/nostr";
import { RootState } from "State/Store";
import Check from "Icons/Check";
import Zap from "Icons/Zap";
@ -18,25 +17,7 @@ import Copy from "Element/Copy";
import useWebln from "Hooks/useWebln";
import messages from "./messages";
interface LNURLService {
nostrPubkey?: HexKey;
minSendable?: number;
maxSendable?: number;
metadata: string;
callback: string;
commentAllowed?: number;
}
interface LNURLInvoice {
pr: string;
successAction?: LNURLSuccessAction;
}
interface LNURLSuccessAction {
description?: string;
url?: string;
}
import { LNURL, LNURLInvoice, LNURLSuccessAction } from "LNURL";
enum ZapType {
PublicZap = 1,
@ -45,9 +26,9 @@ enum ZapType {
NonZap = 4,
}
export interface LNURLTipProps {
export interface SendSatsProps {
onClose?: () => void;
svc?: string;
lnurl?: string;
show?: boolean;
invoice?: string; // shortcut to invoice qr tab
title?: string;
@ -69,10 +50,8 @@ function chunks<T>(arr: T[], length: number) {
return result;
}
export default function LNURLTip(props: LNURLTipProps) {
export default function SendSats(props: SendSatsProps) {
const onClose = props.onClose || (() => undefined);
const service = props.svc;
const show = props.show || false;
const { note, author, target } = props;
const defaultZapAmount = useSelector((s: RootState) => s.login.preferences.defaultZapAmount);
const amounts = [defaultZapAmount, 1_000, 5_000, 10_000, 20_000, 50_000, 100_000, 1_000_000];
@ -85,97 +64,68 @@ export default function LNURLTip(props: LNURLTipProps) {
100_000: "🚀",
1_000_000: "🤯",
};
const [payService, setPayService] = useState<LNURLService>();
const [handler, setHandler] = useState<LNURL>();
const [invoice, setInvoice] = useState(props.invoice);
const [amount, setAmount] = useState<number>(defaultZapAmount);
const [customAmount, setCustomAmount] = useState<number>();
const [invoice, setInvoice] = useState<LNURLInvoice>();
const [comment, setComment] = useState<string>();
const [error, setError] = useState<string>();
const [success, setSuccess] = useState<LNURLSuccessAction>();
const [error, setError] = useState<string>();
const [zapType, setZapType] = useState(ZapType.PublicZap);
const [paying, setPaying] = useState<boolean>(false);
const webln = useWebln(show);
const webln = useWebln(props.show);
const { formatMessage } = useIntl();
const publisher = useEventPublisher();
const canComment = (payService?.commentAllowed ?? 0) > 0 || payService?.nostrPubkey;
const canComment = handler ? (handler.canZap && zapType !== ZapType.NonZap) || handler.maxCommentLength > 0 : false;
useEffect(() => {
if (show && !props.invoice) {
loadService()
.then(a => setPayService(a ?? undefined))
.catch(() => setError(formatMessage(messages.LNURLFail)));
} else {
setPayService(undefined);
if (props.show) {
setError(undefined);
setInvoice(props.invoice ? { pr: props.invoice } : undefined);
setAmount(defaultZapAmount);
setComment(undefined);
setSuccess(undefined);
setZapType(ZapType.PublicZap);
setInvoice(undefined);
setSuccess(undefined);
}
}, [show, service]);
}, [props.show]);
useEffect(() => {
if (props.lnurl && props.show) {
try {
const h = new LNURL(props.lnurl);
setHandler(h);
h.load().catch(e => setError((e as Error).message));
} catch (e) {
if (e instanceof Error) {
setError(e.message);
}
}
}
}, [props.lnurl, props.show]);
const serviceAmounts = useMemo(() => {
if (payService) {
const min = (payService.minSendable ?? 0) / 1000;
const max = (payService.maxSendable ?? 0) / 1000;
if (handler) {
const min = handler.min / 1000;
const max = handler.max / 1000;
return amounts.filter(a => a >= min && a <= max);
}
return [];
}, [payService]);
}, [handler]);
const amountRows = useMemo(() => chunks(serviceAmounts, 3), [serviceAmounts]);
const selectAmount = (a: number) => {
setError(undefined);
setInvoice(undefined);
setAmount(a);
};
async function fetchJson<T>(url: string) {
const rsp = await fetch(url);
if (rsp.ok) {
const data: T = await rsp.json();
console.log(data);
setError(undefined);
return data;
}
return null;
}
async function loadService(): Promise<LNURLService | null> {
if (service) {
const isServiceUrl = service.toLowerCase().startsWith("lnurl");
if (isServiceUrl) {
const serviceUrl = bech32ToText(service);
return await fetchJson(serviceUrl);
} else {
const ns = service.split("@");
return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`);
}
}
return null;
}
async function loadInvoice() {
if (!amount || !payService) return null;
if (!amount || !handler) return null;
const callback = new URL(payService.callback);
const query = new Map<string, string>();
if (callback.search.length > 0) {
callback.search
.slice(1)
.split("&")
.forEach(a => {
const pSplit = a.split("=");
query.set(pSplit[0], pSplit[1]);
});
}
query.set("amount", Math.floor(amount * 1000).toString());
if (comment && payService?.commentAllowed) {
query.set("comment", comment);
}
if (payService.nostrPubkey && author && zapType !== ZapType.NonZap) {
const ev = await publisher.zap(author, note, comment);
let zap: Event | undefined;
if (author && zapType !== ZapType.NonZap) {
const ev = await publisher.zap(amount * 1000, author, note, comment);
if (ev) {
// replace sig for anon-zap
if (zapType === ZapType.AnonZap) {
@ -186,26 +136,15 @@ export default function LNURLTip(props: LNURLTipProps) {
ev.Tags.push(new Tag(["anon"], ev.Tags.length));
await ev.Sign(randomKey.privateKey);
}
query.set("nostr", JSON.stringify(ev.ToObject()));
zap = ev;
}
}
const baseUrl = `${callback.protocol}//${callback.host}${callback.pathname}`;
const queryJoined = [...query.entries()].map(v => `${v[0]}=${encodeURIComponent(v[1])}`).join("&");
try {
const rsp = await fetch(`${baseUrl}?${queryJoined}`);
if (rsp.ok) {
const data = await rsp.json();
console.log(data);
if (data.status === "ERROR") {
setError(data.reason);
} else {
setInvoice(data);
setError("");
payWebLNIfEnabled(data);
}
} else {
setError(formatMessage(messages.InvoiceFail));
const rsp = await handler.getInvoice(amount, comment, zap);
if (rsp.pr) {
setInvoice(rsp.pr);
await payWebLNIfEnabled(rsp);
}
} catch (e) {
setError(formatMessage(messages.InvoiceFail));
@ -213,8 +152,10 @@ export default function LNURLTip(props: LNURLTipProps) {
}
function custom() {
const min = (payService?.minSendable ?? 1000) / 1000;
const max = (payService?.maxSendable ?? 21_000_000_000) / 1000;
if (!handler) return null;
const min = handler.min / 1000;
const max = handler.max / 1000;
return (
<div className="custom-amount flex">
<input
@ -269,21 +210,21 @@ export default function LNURLTip(props: LNURLTipProps) {
}
function invoiceForm() {
if (invoice) return null;
if (!handler || invoice) return null;
return (
<>
<h3>
<FormattedMessage {...messages.ZapAmount} />
</h3>
{amountRows.map(amounts => renderAmounts(amount, amounts))}
{payService && custom()}
{custom()}
<div className="flex">
{canComment && (
<input
type="text"
placeholder={formatMessage(messages.Comment)}
className="f-grow"
maxLength={payService?.commentAllowed || 120}
maxLength={handler.canZap && zapType !== ZapType.NonZap ? 250 : handler.maxCommentLength}
onChange={e => setComment(e.target.value)}
/>
)}
@ -306,9 +247,9 @@ export default function LNURLTip(props: LNURLTipProps) {
}
function zapTypeSelector() {
if (!payService || !payService.nostrPubkey) return;
if (!handler || !handler.canZap) return;
const makeTab = (t: ZapType, n: string) => (
const makeTab = (t: ZapType, n: React.ReactNode) => (
<div className={`tab${zapType === t ? " active" : ""}`} onClick={() => setZapType(t)}>
{n}
</div>
@ -319,18 +260,20 @@ export default function LNURLTip(props: LNURLTipProps) {
<FormattedMessage defaultMessage="Zap Type" />
</h3>
<div className="tabs mt10">
{makeTab(ZapType.PublicZap, "Public")}
{makeTab(ZapType.PublicZap, <FormattedMessage defaultMessage="Public" description="Public Zap" />)}
{/*makeTab(ZapType.PrivateZap, "Private")*/}
{makeTab(ZapType.AnonZap, "Anon")}
{makeTab(ZapType.NonZap, "Non-Zap")}
{makeTab(ZapType.AnonZap, <FormattedMessage defaultMessage="Anon" description="Anonymous Zap" />)}
{makeTab(
ZapType.NonZap,
<FormattedMessage defaultMessage="Non-Zap" description="Non-Zap, Regular LN payment" />
)}
</div>
</>
);
}
function payInvoice() {
if (success) return null;
const pr = invoice?.pr;
if (success || !invoice) return null;
return (
<>
<div className="invoice">
@ -341,15 +284,15 @@ export default function LNURLTip(props: LNURLTipProps) {
...
</h4>
) : (
<QrCode data={pr} link={`lightning:${pr}`} />
<QrCode data={invoice} link={`lightning:${invoice}`} />
)}
<div className="actions">
{pr && (
{invoice && (
<>
<div className="copy-action">
<Copy text={pr} maxSize={26} />
<Copy text={invoice} maxSize={26} />
</div>
<button className="wallet-action" type="button" onClick={() => window.open(`lightning:${pr}`)}>
<button className="wallet-action" type="button" onClick={() => window.open(`lightning:${invoice}`)}>
<FormattedMessage {...messages.OpenWallet} />
</button>
</>
@ -379,14 +322,14 @@ export default function LNURLTip(props: LNURLTipProps) {
);
}
const defaultTitle = payService?.nostrPubkey ? formatMessage(messages.SendZap) : formatMessage(messages.SendSats);
const defaultTitle = handler?.canZap ? formatMessage(messages.SendZap) : formatMessage(messages.SendSats);
const title = target
? formatMessage(messages.ToTarget, {
action: defaultTitle,
target,
})
: defaultTitle;
if (!show) return null;
if (!(props.show ?? false)) return null;
return (
<Modal className="lnurl-modal" onClose={onClose}>
<div className="lnurl-tip" onClick={e => e.stopPropagation()}>

View File

@ -2,25 +2,30 @@ import "./ZapButton.css";
import { faBolt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react";
import { useLongPress } from "use-long-press";
import { useUserProfile } from "Feed/ProfileFeed";
import { HexKey } from "@snort/nostr";
import SendSats from "Element/SendSats";
const ZapButton = ({ pubkey, svc }: { pubkey: HexKey; svc?: string }) => {
const ZapButton = ({ pubkey, lnurl }: { pubkey: HexKey; lnurl?: string }) => {
const profile = useUserProfile(pubkey);
const [zap, setZap] = useState(false);
const service = svc ?? (profile?.lud16 || profile?.lud06);
const service = lnurl ?? (profile?.lud16 || profile?.lud06);
const longPress = useLongPress(() => {
console.debug("long press");
});
if (!service) return null;
return (
<>
<div className="zap-button" onClick={() => setZap(true)}>
<div className="zap-button" {...longPress()}>
<FontAwesomeIcon icon={faBolt} />
</div>
<SendSats
target={profile?.display_name || profile?.name}
svc={service}
lnurl={service}
show={zap}
onClose={() => setZap(false)}
author={pubkey}

View File

@ -191,7 +191,7 @@ export default function useEventPublisher() {
return await signEvent(ev);
}
},
zap: async (author: HexKey, note?: HexKey, msg?: string) => {
zap: async (amount: number, author: HexKey, note?: HexKey, msg?: string) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ZapRequest;
@ -201,6 +201,7 @@ export default function useEventPublisher() {
ev.Tags.push(new Tag(["p", author], ev.Tags.length));
const relayTag = ["relays", ...Object.keys(relays)];
ev.Tags.push(new Tag(relayTag, ev.Tags.length));
ev.Tags.push(new Tag(["amount", amount.toString()], ev.Tags.length));
processContent(ev, msg || "");
return await signEvent(ev);
}

View File

@ -0,0 +1,33 @@
.spinner_V8m1 {
transform-origin: center;
animation: spinner_zKoa 2s linear infinite;
}
.spinner_V8m1 circle {
stroke-linecap: round;
animation: spinner_YpZS 1.5s ease-in-out infinite;
}
@keyframes spinner_zKoa {
100% {
transform: rotate(360deg);
}
}
@keyframes spinner_YpZS {
0% {
stroke-dasharray: 0 150;
stroke-dashoffset: 0;
}
47.5% {
stroke-dasharray: 42 150;
stroke-dashoffset: -16;
}
95%,
100% {
stroke-dasharray: 42 150;
stroke-dashoffset: -59;
}
}

View File

@ -0,0 +1,12 @@
import IconProps from "./IconProps";
import "./Spinner.css";
const Spinner = (props: IconProps) => (
<svg width="20" height="20" stroke="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
<g className="spinner_V8m1">
<circle cx="10" cy="10" r="7.5" fill="none" stroke-width="3"></circle>
</g>
</svg>
);
export default Spinner;

View File

@ -0,0 +1,17 @@
import IconProps from "./IconProps";
const ZapFast = (props: IconProps) => {
return (
<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M8 15.5H2.5M5.5 10H1M8 4.5H3M16 1L9.40357 10.235C9.1116 10.6438 8.96562 10.8481 8.97194 11.0185C8.97744 11.1669 9.04858 11.3051 9.1661 11.3958C9.30108 11.5 9.55224 11.5 10.0546 11.5H15L14 19L20.5964 9.76499C20.8884 9.35624 21.0344 9.15187 21.0281 8.98147C21.0226 8.83312 20.9514 8.69489 20.8339 8.60418C20.6989 8.5 20.4478 8.5 19.9454 8.5H15L16 1Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export default ZapFast;

145
packages/app/src/LNURL.ts Normal file
View File

@ -0,0 +1,145 @@
import { Event, HexKey } from "@snort/nostr";
import { EmailRegex } from "Const";
import { bech32ToText, unwrap } from "Util";
const PayServiceTag = "payRequest";
export class LNURL {
#url: URL;
#service?: LNURLService;
constructor(lnurl: string) {
if (lnurl.toLowerCase().startsWith("lnurl")) {
const decoded = bech32ToText(lnurl);
if (!decoded.startsWith("http")) {
throw new Error("Invalid LNURL: not a url");
}
this.#url = new URL(decoded);
} else if (lnurl.match(EmailRegex)) {
const [handle, domain] = lnurl.toLowerCase().split("@");
this.#url = new URL(`https://${domain}/.well-known/lnurlp/${handle}`);
} else if (lnurl.toLowerCase().startsWith("http")) {
this.#url = new URL(lnurl);
} else {
throw new Error("Invalid LNURL: could not determine service url");
}
}
async load() {
const rsp = await fetch(this.#url);
if (rsp.ok) {
this.#service = await rsp.json();
this.#validateService();
}
}
/**
* Fetch an invoice from the LNURL service
* @param amount Amount in sats
* @param comment
* @param zap
* @returns
*/
async getInvoice(amount: number, comment?: string, zap?: Event) {
const callback = new URL(unwrap(this.#service?.callback));
const query = new Map<string, string>();
if (callback.search.length > 0) {
callback.search
.slice(1)
.split("&")
.forEach(a => {
const pSplit = a.split("=");
query.set(pSplit[0], pSplit[1]);
});
}
query.set("amount", Math.floor(amount * 1000).toString());
if (comment && this.#service?.commentAllowed) {
query.set("comment", comment);
}
if (this.#service?.nostrPubkey && zap) {
query.set("nostr", JSON.stringify(zap.ToObject()));
}
const baseUrl = `${callback.protocol}//${callback.host}${callback.pathname}`;
const queryJoined = [...query.entries()].map(v => `${v[0]}=${encodeURIComponent(v[1])}`).join("&");
try {
const rsp = await fetch(`${baseUrl}?${queryJoined}`);
if (rsp.ok) {
const data: LNURLInvoice = await rsp.json();
console.debug("[LNURL]: ", data);
if (data.status === "ERROR") {
throw new Error(data.reason);
} else {
return data;
}
} else {
throw new Error(`Failed to fetch invoice (${rsp.statusText})`);
}
} catch (e) {
throw new Error("Failed to load callback");
}
}
/**
* Are zaps (NIP-57) supported
*/
get canZap() {
return this.#service?.nostrPubkey ? true : false;
}
/**
* Get the max allowed comment length
*/
get maxCommentLength() {
return this.#service?.commentAllowed ?? 0;
}
/**
* Min sendable in milli-sats
*/
get min() {
return this.#service?.minSendable ?? 1_000; // 1 sat
}
/**
* Max sendable in milli-sats
*/
get max() {
return this.#service?.maxSendable ?? 100e9; // 1 BTC in milli-sats
}
#validateService() {
if (this.#service?.tag !== PayServiceTag) {
throw new Error("Invalid service: only lnurlp is supported");
}
if (!this.#service?.callback) {
throw new Error("Invalid service: no callback url");
}
}
}
export interface LNURLService {
tag: string;
nostrPubkey?: HexKey;
minSendable?: number;
maxSendable?: number;
metadata: string;
callback: string;
commentAllowed?: number;
}
export interface LNURLStatus {
status: "SUCCESS" | "ERROR";
reason?: string;
}
export interface LNURLInvoice extends LNURLStatus {
pr?: string;
successAction?: LNURLSuccessAction;
}
export interface LNURLSuccessAction {
description?: string;
url?: string;
}

View File

@ -100,7 +100,7 @@ const DonatePage = () => {
<div className="mr10">
<FormattedMessage defaultMessage="Lightning Donation: " />
</div>
<ZapButton pubkey={bech32ToHex(SnortPubKey)} svc={"donate@snort.social"} />
<ZapButton pubkey={bech32ToHex(SnortPubKey)} lnurl={"donate@snort.social"} />
</div>
{today && (
<small>

View File

@ -160,7 +160,7 @@ export default function ProfilePage() {
)}
<SendSats
svc={lnurl}
lnurl={lnurl}
show={showLnQr}
onClose={() => setShowLnQr(false)}
author={id}

View File

@ -14,6 +14,10 @@
"/JE/X+": {
"defaultMessage": "Account Support"
},
"/PCavi": {
"defaultMessage": "Public",
"description": "Public Zap"
},
"/RD0e2": {
"defaultMessage": "Nostr uses digital signature technology to provide tamper proof notes which can safely be replicated to many relays to provide redundant storage of your content."
},
@ -162,6 +166,10 @@
"Adk34V": {
"defaultMessage": "Setup your Profile"
},
"AnLrRC": {
"defaultMessage": "Non-Zap",
"description": "Non-Zap, Regular LN payment"
},
"AyGauy": {
"defaultMessage": "Login"
},
@ -755,6 +763,10 @@
"defaultMessage": "Your key",
"description": "Label for key input"
},
"wWLwvh": {
"defaultMessage": "Anon",
"description": "Anonymous Zap"
},
"wih7iJ": {
"defaultMessage": "name is blocked"
},

View File

@ -4,6 +4,7 @@
"+vIQlC": "Please make sure to save the following password in order to manage your handle in the future",
"/4tOwT": "Skip",
"/JE/X+": "Account Support",
"/PCavi": "Public",
"/RD0e2": "Nostr uses digital signature technology to provide tamper proof notes which can safely be replicated to many relays to provide redundant storage of your content.",
"/d6vEc": "Make your profile easier to find and share",
"/n5KSF": "{n} ms",
@ -52,6 +53,7 @@
"ADmfQT": "Parent",
"ASRK0S": "This author has been muted",
"Adk34V": "Setup your Profile",
"AnLrRC": "Non-Zap",
"AyGauy": "Login",
"B4C47Y": "name too short",
"B6+XJy": "zapped",
@ -246,6 +248,7 @@
"vZ4quW": "NIP-05 is a DNS based verification spec which helps to validate you as a real user.",
"wEQDC6": "Edit",
"wLtRCF": "Your key",
"wWLwvh": "Anon",
"wih7iJ": "name is blocked",
"wqyN/i": "Find out more info about {service} at {link}",
"wtLjP6": "Copy ID",

View File

@ -9859,6 +9859,11 @@ use-latest@^1.2.1:
dependencies:
use-isomorphic-layout-effect "^1.1.1"
use-long-press@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/use-long-press/-/use-long-press-2.0.3.tgz#9e26da35f05819fe8056787c78a101e91e3e464b"
integrity sha512-n3cfv90Y1ldNt+hhXzxnxuLZmgLOOC/+qfLGoeEBgOxmnokPPt39MPF3KmvKriq5VMoJ7uQdVjHejCdHBt9anw==
use-sync-external-store@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"