Zap splits
This commit is contained in:
parent
4864ef6831
commit
d2baf9bd5b
@ -18,3 +18,13 @@
|
|||||||
.avatar[data-domain="strike.army"] {
|
.avatar[data-domain="strike.army"] {
|
||||||
background-image: var(--img-url), var(--strike-army-gradient);
|
background-image: var(--img-url), var(--strike-army-gradient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar .overlay {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import "./Avatar.css";
|
import "./Avatar.css";
|
||||||
|
|
||||||
import { CSSProperties, useEffect, useState } from "react";
|
import { CSSProperties, ReactNode, useEffect, useState } from "react";
|
||||||
import type { UserMetadata } from "@snort/system";
|
import type { UserMetadata } from "@snort/system";
|
||||||
|
|
||||||
import useImgProxy from "Hooks/useImgProxy";
|
import useImgProxy from "Hooks/useImgProxy";
|
||||||
@ -13,8 +13,9 @@ interface AvatarProps {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
size?: number;
|
size?: number;
|
||||||
image?: string;
|
image?: string;
|
||||||
|
imageOverlay?: ReactNode;
|
||||||
}
|
}
|
||||||
const Avatar = ({ pubkey, user, size, onClick, image }: AvatarProps) => {
|
const Avatar = ({ pubkey, user, size, onClick, image, imageOverlay }: AvatarProps) => {
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
const { proxy } = useImgProxy();
|
const { proxy } = useImgProxy();
|
||||||
|
|
||||||
@ -35,9 +36,11 @@ const Avatar = ({ pubkey, user, size, onClick, image }: AvatarProps) => {
|
|||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={style}
|
style={style}
|
||||||
className="avatar"
|
className={`avatar${imageOverlay ? " with-overlay" : ""}`}
|
||||||
data-domain={domain?.toLowerCase()}
|
data-domain={domain?.toLowerCase()}
|
||||||
title={getDisplayName(user, "")}></div>
|
title={getDisplayName(user, "")}>
|
||||||
|
{imageOverlay && <div className="overlay">{imageOverlay}</div>}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,14 +1,5 @@
|
|||||||
.copy {
|
.copy .copy-body {
|
||||||
cursor: pointer;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy .body {
|
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
color: var(--font-color);
|
color: var(--font-color);
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy .icon {
|
|
||||||
margin-bottom: -4px;
|
|
||||||
}
|
|
||||||
|
@ -13,8 +13,8 @@ export default function Copy({ text, maxSize = 32, className }: CopyProps) {
|
|||||||
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text;
|
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-row copy ${className}`} onClick={() => copy(text)}>
|
<div className={`copy flex pointer g8${className ? ` ${className}` : ""}`} onClick={() => copy(text)}>
|
||||||
<span className="body">{trimmed}</span>
|
<span className="copy-body">{trimmed}</span>
|
||||||
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
|
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
|
||||||
{copied ? <Icon name="check" size={14} /> : <Icon name="copy-solid" size={14} />}
|
{copied ? <Icon name="check" size={14} /> : <Icon name="copy-solid" size={14} />}
|
||||||
</span>
|
</span>
|
||||||
|
@ -75,7 +75,7 @@ export default function Invoice(props: InvoiceProps) {
|
|||||||
{description && <p>{description}</p>}
|
{description && <p>{description}</p>}
|
||||||
{isPaid ? (
|
{isPaid ? (
|
||||||
<div className="paid">
|
<div className="paid">
|
||||||
<FormattedMessage {...messages.Paid} />
|
<FormattedMessage defaultMessage="Paid" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button disabled={isExpired} type="button" onClick={payInvoice}>
|
<button disabled={isExpired} type="button" onClick={payInvoice}>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
import "./NoteCreator.css";
|
import "./NoteCreator.css";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { encodeTLV, EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder } from "@snort/system";
|
import { encodeTLV, EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink } from "@snort/system";
|
||||||
import { LNURL } from "@snort/shared";
|
|
||||||
|
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
@ -21,7 +19,7 @@ import {
|
|||||||
setPreview,
|
setPreview,
|
||||||
setShowAdvanced,
|
setShowAdvanced,
|
||||||
setSelectedCustomRelays,
|
setSelectedCustomRelays,
|
||||||
setZapForward,
|
setZapSplits,
|
||||||
setSensitive,
|
setSensitive,
|
||||||
reset,
|
reset,
|
||||||
setPollOptions,
|
setPollOptions,
|
||||||
@ -29,16 +27,13 @@ import {
|
|||||||
} from "State/NoteCreator";
|
} from "State/NoteCreator";
|
||||||
import type { RootState } from "State/Store";
|
import type { RootState } from "State/Store";
|
||||||
|
|
||||||
import messages from "./messages";
|
import { ClipboardEventHandler } from "react";
|
||||||
import { ClipboardEventHandler, useState } from "react";
|
|
||||||
import Spinner from "Icons/Spinner";
|
|
||||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
|
||||||
import { LoginStore } from "Login";
|
|
||||||
import { getCurrentSubscription } from "Subscription";
|
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import { AsyncIcon } from "Element/AsyncIcon";
|
import { AsyncIcon } from "Element/AsyncIcon";
|
||||||
|
import { fetchNip05Pubkey } from "@snort/shared";
|
||||||
|
import { ZapTarget } from "Zapper";
|
||||||
|
|
||||||
export function NoteCreator() {
|
export function NoteCreator() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@ -46,7 +41,7 @@ export function NoteCreator() {
|
|||||||
const uploader = useFileUpload();
|
const uploader = useFileUpload();
|
||||||
const {
|
const {
|
||||||
note,
|
note,
|
||||||
zapForward,
|
zapSplits,
|
||||||
sensitive,
|
sensitive,
|
||||||
pollOptions,
|
pollOptions,
|
||||||
replyTo,
|
replyTo,
|
||||||
@ -59,45 +54,95 @@ export function NoteCreator() {
|
|||||||
error,
|
error,
|
||||||
} = useSelector((s: RootState) => s.noteCreator);
|
} = useSelector((s: RootState) => s.noteCreator);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const sub = getCurrentSubscription(LoginStore.allSubscriptions());
|
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const relays = login.relays;
|
const relays = login.relays;
|
||||||
|
|
||||||
async function sendNote() {
|
async function buildNote() {
|
||||||
if (note && publisher) {
|
try {
|
||||||
let extraTags: Array<Array<string>> | undefined;
|
dispatch(setError(""));
|
||||||
if (zapForward) {
|
if (note && publisher) {
|
||||||
try {
|
let extraTags: Array<Array<string>> | undefined;
|
||||||
const svc = new LNURL(zapForward);
|
if (zapSplits) {
|
||||||
await svc.load();
|
const parsedSplits = [] as Array<ZapTarget>;
|
||||||
extraTags = [svc.getZapTag()];
|
for (const s of zapSplits) {
|
||||||
} catch {
|
if (s.value.startsWith(NostrPrefix.PublicKey) || s.value.startsWith(NostrPrefix.Profile)) {
|
||||||
dispatch(
|
const link = tryParseNostrLink(s.value);
|
||||||
setError(
|
if (link) {
|
||||||
formatMessage({
|
parsedSplits.push({ ...s, value: link.id });
|
||||||
defaultMessage: "Invalid LNURL",
|
} else {
|
||||||
}),
|
throw new Error(
|
||||||
),
|
formatMessage(
|
||||||
);
|
{
|
||||||
return;
|
defaultMessage: "Failed to parse zap split: {input}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: s.value,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (s.value.includes("@")) {
|
||||||
|
const [name, domain] = s.value.split("@");
|
||||||
|
const pubkey = await fetchNip05Pubkey(name, domain);
|
||||||
|
if (pubkey) {
|
||||||
|
parsedSplits.push({ ...s, value: pubkey });
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Failed to parse zap split: {input}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: s.value,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Invalid zap split: {input}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: s.value,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extraTags = parsedSplits.map(v => ["zap", v.value, "", String(v.weight)]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
extraTags ??= [];
|
extraTags ??= [];
|
||||||
extraTags.push(["content-warning", sensitive]);
|
extraTags.push(["content-warning", sensitive]);
|
||||||
|
}
|
||||||
|
const kind = pollOptions ? EventKind.Polls : EventKind.TextNote;
|
||||||
|
if (pollOptions) {
|
||||||
|
extraTags ??= [];
|
||||||
|
extraTags.push(...pollOptions.map((a, i) => ["poll_option", i.toString(), a]));
|
||||||
|
}
|
||||||
|
const hk = (eb: EventBuilder) => {
|
||||||
|
extraTags?.forEach(t => eb.tag(t));
|
||||||
|
eb.kind(kind);
|
||||||
|
return eb;
|
||||||
|
};
|
||||||
|
const ev = replyTo ? await publisher.reply(replyTo, note, hk) : await publisher.note(note, hk);
|
||||||
|
return ev;
|
||||||
}
|
}
|
||||||
const kind = pollOptions ? EventKind.Polls : EventKind.TextNote;
|
} catch (e) {
|
||||||
if (pollOptions) {
|
if (e instanceof Error) {
|
||||||
extraTags ??= [];
|
dispatch(setError(e.message));
|
||||||
extraTags.push(...pollOptions.map((a, i) => ["poll_option", i.toString(), a]));
|
} else {
|
||||||
|
dispatch(setError(e as string));
|
||||||
}
|
}
|
||||||
const hk = (eb: EventBuilder) => {
|
}
|
||||||
extraTags?.forEach(t => eb.tag(t));
|
}
|
||||||
eb.kind(kind);
|
|
||||||
return eb;
|
async function sendNote() {
|
||||||
};
|
const ev = await buildNote();
|
||||||
const ev = replyTo ? await publisher.reply(replyTo, note, hk) : await publisher.note(note, hk);
|
if (ev) {
|
||||||
if (selectedCustomRelays) selectedCustomRelays.forEach(r => System.WriteOnceToRelay(r, ev));
|
if (selectedCustomRelays) selectedCustomRelays.forEach(r => System.WriteOnceToRelay(r, ev));
|
||||||
else System.BroadcastEvent(ev);
|
else System.BroadcastEvent(ev);
|
||||||
dispatch(reset());
|
dispatch(reset());
|
||||||
@ -166,7 +211,7 @@ export function NoteCreator() {
|
|||||||
if (preview) {
|
if (preview) {
|
||||||
dispatch(setPreview(undefined));
|
dispatch(setPreview(undefined));
|
||||||
} else if (publisher) {
|
} else if (publisher) {
|
||||||
const tmpNote = await publisher.note(note);
|
const tmpNote = await buildNote();
|
||||||
if (tmpNote) {
|
if (tmpNote) {
|
||||||
dispatch(setPreview(tmpNote));
|
dispatch(setPreview(tmpNote));
|
||||||
}
|
}
|
||||||
@ -269,7 +314,7 @@ export function NoteCreator() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listAccounts() {
|
/*function listAccounts() {
|
||||||
return LoginStore.getSessions().map(a => (
|
return LoginStore.getSessions().map(a => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={ev => {
|
onClick={ev => {
|
||||||
@ -279,7 +324,7 @@ export function NoteCreator() {
|
|||||||
<ProfileImage pubkey={a} link={""} />
|
<ProfileImage pubkey={a} link={""} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
));
|
));
|
||||||
}
|
}*/
|
||||||
|
|
||||||
const handlePaste: ClipboardEventHandler<HTMLDivElement> = evt => {
|
const handlePaste: ClipboardEventHandler<HTMLDivElement> = evt => {
|
||||||
if (evt.clipboardData) {
|
if (evt.clipboardData) {
|
||||||
@ -374,20 +419,63 @@ export function NoteCreator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-column g8">
|
<div className="flex-column g8">
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Forward Zaps" />
|
<FormattedMessage defaultMessage="Zap Splits" />
|
||||||
</h4>
|
</h4>
|
||||||
<FormattedMessage defaultMessage="All zaps sent to this note will be received by the following LNURL" />
|
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." />
|
||||||
<input
|
<div className="flex-column g8">
|
||||||
type="text"
|
{[...(zapSplits ?? [])].map((v, i, arr) => (
|
||||||
className="w-max"
|
<div className="flex f-center g8">
|
||||||
placeholder={formatMessage({
|
<div className="flex-column f-4 g4">
|
||||||
defaultMessage: "LNURL to forward zaps to",
|
<h4>
|
||||||
})}
|
<FormattedMessage defaultMessage="Recipient" />
|
||||||
value={zapForward}
|
</h4>
|
||||||
onChange={e => dispatch(setZapForward(e.target.value))}
|
<input
|
||||||
/>
|
type="text"
|
||||||
|
value={v.value}
|
||||||
|
onChange={e =>
|
||||||
|
dispatch(
|
||||||
|
setZapSplits(arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address" })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-column f-1 g4">
|
||||||
|
<h4>
|
||||||
|
<FormattedMessage defaultMessage="Weight" />
|
||||||
|
</h4>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={v.weight}
|
||||||
|
onChange={e =>
|
||||||
|
dispatch(
|
||||||
|
setZapSplits(
|
||||||
|
arr.map((vv, ii) => (ii === i ? { ...vv, weight: Number(e.target.value) } : vv)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-column f-shrink g4">
|
||||||
|
<div> </div>
|
||||||
|
<Icon
|
||||||
|
name="close"
|
||||||
|
onClick={() => dispatch(setZapSplits((zapSplits ?? []).filter((_v, ii) => ii !== i)))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
dispatch(setZapSplits([...(zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }]))
|
||||||
|
}>
|
||||||
|
<FormattedMessage defaultMessage="Add" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<span className="warning">
|
<span className="warning">
|
||||||
<FormattedMessage defaultMessage="Not all clients support this yet" />
|
<FormattedMessage defaultMessage="Not all clients support this, you may still receive some zaps as if zap splits was not configured" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-column g8">
|
<div className="flex-column g8">
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import React, { HTMLProps, useEffect, useState } from "react";
|
import React, { HTMLProps, useContext, useEffect, useState } from "react";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { useLongPress } from "use-long-press";
|
import { useLongPress } from "use-long-press";
|
||||||
import { TaggedNostrEvent, HexKey, u256, ParsedZap, countLeadingZeros } from "@snort/system";
|
import { TaggedNostrEvent, ParsedZap, countLeadingZeros, createNostrLinkToEvent } from "@snort/system";
|
||||||
import { LNURL } from "@snort/shared";
|
import { SnortContext, useUserProfile } from "@snort/system-react";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
|
||||||
|
|
||||||
import { formatShort } from "Number";
|
import { formatShort } from "Number";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import { delay, findTag, normalizeReaction, unwrap } from "SnortUtils";
|
import { delay, findTag, normalizeReaction } from "SnortUtils";
|
||||||
import { NoteCreator } from "Element/NoteCreator";
|
import { NoteCreator } from "Element/NoteCreator";
|
||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
import { ZapsSummary } from "Element/Zap";
|
import { ZapsSummary } from "Element/Zap";
|
||||||
@ -21,6 +20,8 @@ import useLogin from "Hooks/useLogin";
|
|||||||
import { useInteractionCache } from "Hooks/useInteractionCache";
|
import { useInteractionCache } from "Hooks/useInteractionCache";
|
||||||
import { ZapPoolController } from "ZapPoolController";
|
import { ZapPoolController } from "ZapPoolController";
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
|
import { Zapper, ZapTarget } from "Zapper";
|
||||||
|
import { getDisplayName } from "./ProfileImage";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
@ -47,9 +48,10 @@ export interface NoteFooterProps {
|
|||||||
export default function NoteFooter(props: NoteFooterProps) {
|
export default function NoteFooter(props: NoteFooterProps) {
|
||||||
const { ev, positive, reposts, zaps } = props;
|
const { ev, positive, reposts, zaps } = props;
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const system = useContext(SnortContext);
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const { publicKey, preferences: prefs, relays } = login;
|
const { publicKey, preferences: prefs } = login;
|
||||||
const author = useUserProfile(ev.pubkey);
|
const author = useUserProfile(ev.pubkey);
|
||||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
const interactionCache = useInteractionCache(publicKey, ev.id);
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
@ -103,31 +105,36 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLNURL() {
|
function getZapTarget(): Array<ZapTarget> | undefined {
|
||||||
return ev.tags.find(a => a[0] === "zap")?.[1] || author?.lud16 || author?.lud06;
|
if (ev.tags.some(v => v[0] === "zap")) {
|
||||||
}
|
return Zapper.fromEvent(ev);
|
||||||
|
}
|
||||||
|
|
||||||
function getTargetName() {
|
const authorTarget = author?.lud16 || author?.lud06;
|
||||||
const zapTarget = ev.tags.find(a => a[0] === "zap")?.[1];
|
if (authorTarget) {
|
||||||
if (zapTarget) {
|
return [
|
||||||
try {
|
{
|
||||||
return new LNURL(zapTarget).name;
|
type: "lnurl",
|
||||||
} catch {
|
value: authorTarget,
|
||||||
// ignore
|
weight: 1,
|
||||||
}
|
name: getDisplayName(author, ev.pubkey),
|
||||||
} else {
|
zap: {
|
||||||
return author?.display_name || author?.name;
|
pubkey: ev.pubkey,
|
||||||
|
event: createNostrLinkToEvent(ev),
|
||||||
|
},
|
||||||
|
} as ZapTarget,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fastZap(e?: React.MouseEvent) {
|
async function fastZap(e?: React.MouseEvent) {
|
||||||
if (zapping || e?.isPropagationStopped()) return;
|
if (zapping || e?.isPropagationStopped()) return;
|
||||||
|
|
||||||
const lnurl = getLNURL();
|
const lnurl = getZapTarget();
|
||||||
if (wallet?.isReady() && lnurl) {
|
if (wallet?.isReady() && lnurl) {
|
||||||
setZapping(true);
|
setZapping(true);
|
||||||
try {
|
try {
|
||||||
await fastZapInner(lnurl, prefs.defaultZapAmount, ev.pubkey, ev.id);
|
await fastZapInner(lnurl, prefs.defaultZapAmount);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Fast zap failed", e);
|
console.warn("Fast zap failed", e);
|
||||||
if (!(e instanceof Error) || e.message !== "User rejected") {
|
if (!(e instanceof Error) || e.message !== "User rejected") {
|
||||||
@ -141,30 +148,29 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fastZapInner(lnurl: string, amount: number, key: HexKey, id?: u256) {
|
async function fastZapInner(targets: Array<ZapTarget>, amount: number) {
|
||||||
// only allow 1 invoice req/payment at a time to avoid hitting rate limits
|
if (wallet) {
|
||||||
await barrierZapper(async () => {
|
// only allow 1 invoice req/payment at a time to avoid hitting rate limits
|
||||||
const handler = new LNURL(lnurl);
|
await barrierZapper(async () => {
|
||||||
await handler.load();
|
const zapper = new Zapper(system, publisher);
|
||||||
|
const result = await zapper.send(wallet, targets, amount);
|
||||||
const zr = Object.keys(relays.item);
|
const totalSent = result.reduce((acc, v) => (acc += v.sent), 0);
|
||||||
const zap = handler.canZap && publisher ? await publisher.zap(amount * 1000, key, zr, id) : undefined;
|
if (totalSent > 0) {
|
||||||
const invoice = await handler.getInvoice(amount, undefined, zap);
|
ZapPoolController.allocate(totalSent);
|
||||||
await wallet?.payInvoice(unwrap(invoice.pr));
|
await interactionCache.zap();
|
||||||
ZapPoolController.allocate(amount);
|
}
|
||||||
|
});
|
||||||
await interactionCache.zap();
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prefs.autoZap && !didZap && !isMine && !zapping) {
|
if (prefs.autoZap && !didZap && !isMine && !zapping) {
|
||||||
const lnurl = getLNURL();
|
const lnurl = getZapTarget();
|
||||||
if (wallet?.isReady() && lnurl) {
|
if (wallet?.isReady() && lnurl) {
|
||||||
setZapping(true);
|
setZapping(true);
|
||||||
queueMicrotask(async () => {
|
queueMicrotask(async () => {
|
||||||
try {
|
try {
|
||||||
await fastZapInner(lnurl, prefs.defaultZapAmount, ev.pubkey, ev.id);
|
await fastZapInner(lnurl, prefs.defaultZapAmount);
|
||||||
} catch {
|
} catch {
|
||||||
// ignored
|
// ignored
|
||||||
} finally {
|
} finally {
|
||||||
@ -185,8 +191,8 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tipButton() {
|
function tipButton() {
|
||||||
const service = getLNURL();
|
const targets = getZapTarget();
|
||||||
if (service) {
|
if (targets) {
|
||||||
return (
|
return (
|
||||||
<AsyncFooterIcon
|
<AsyncFooterIcon
|
||||||
className={didZap ? "reacted" : ""}
|
className={didZap ? "reacted" : ""}
|
||||||
@ -262,11 +268,10 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
</div>
|
</div>
|
||||||
{willRenderNoteCreator && <NoteCreator />}
|
{willRenderNoteCreator && <NoteCreator />}
|
||||||
<SendSats
|
<SendSats
|
||||||
lnurl={getLNURL()}
|
targets={getZapTarget()}
|
||||||
onClose={() => setTip(false)}
|
onClose={() => setTip(false)}
|
||||||
show={tip}
|
show={tip}
|
||||||
author={author?.pubkey}
|
author={author?.pubkey}
|
||||||
target={getTargetName()}
|
|
||||||
note={ev.id}
|
note={ev.id}
|
||||||
allocatePool={true}
|
allocatePool={true}
|
||||||
/>
|
/>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import "./ProfileImage.css";
|
import "./ProfileImage.css";
|
||||||
|
|
||||||
import React, { useMemo } from "react";
|
import React, { ReactNode, useMemo } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { HexKey, NostrPrefix, UserMetadata } from "@snort/system";
|
import { HexKey, NostrPrefix, UserMetadata } from "@snort/system";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
@ -21,6 +21,7 @@ export interface ProfileImageProps {
|
|||||||
profile?: UserMetadata;
|
profile?: UserMetadata;
|
||||||
size?: number;
|
size?: number;
|
||||||
onClick?: (e: React.MouseEvent) => void;
|
onClick?: (e: React.MouseEvent) => void;
|
||||||
|
imageOverlay?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProfileImage({
|
export default function ProfileImage({
|
||||||
@ -34,6 +35,7 @@ export default function ProfileImage({
|
|||||||
overrideUsername,
|
overrideUsername,
|
||||||
profile,
|
profile,
|
||||||
size,
|
size,
|
||||||
|
imageOverlay,
|
||||||
onClick,
|
onClick,
|
||||||
}: ProfileImageProps) {
|
}: ProfileImageProps) {
|
||||||
const user = useUserProfile(profile ? "" : pubkey) ?? profile;
|
const user = useUserProfile(profile ? "" : pubkey) ?? profile;
|
||||||
@ -54,7 +56,7 @@ export default function ProfileImage({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="avatar-wrapper">
|
<div className="avatar-wrapper">
|
||||||
<Avatar pubkey={pubkey} user={user} size={size} />
|
<Avatar pubkey={pubkey} user={user} size={size} imageOverlay={imageOverlay} />
|
||||||
</div>
|
</div>
|
||||||
{showUsername && (
|
{showUsername && (
|
||||||
<div className="f-ellipsis">
|
<div className="f-ellipsis">
|
||||||
|
@ -1,31 +1,32 @@
|
|||||||
.lnurl-modal .modal-body {
|
.lnurl-modal .modal-body {
|
||||||
padding: 0;
|
padding: 12px 24px;
|
||||||
max-width: 470px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lnurl-modal .lnurl-tip .pfp .avatar {
|
.lnurl-modal .pfp .avatar {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lnurl-tip {
|
|
||||||
padding: 24px 32px;
|
|
||||||
background-color: #1b1b1b;
|
|
||||||
border-radius: 16px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.lnurl-tip {
|
.lnurl-modal {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .lnurl-tip {
|
.light .lnurl-modal {
|
||||||
background-color: var(--gray-superdark);
|
background-color: var(--gray-superdark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lnurl-tip h3 {
|
.lnurl-modal h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lnurl-modal h3 {
|
||||||
|
margin: 0;
|
||||||
color: var(--font-secondary-color);
|
color: var(--font-secondary-color);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
letter-spacing: 0.11em;
|
letter-spacing: 0.11em;
|
||||||
@ -34,43 +35,14 @@
|
|||||||
text-transform: uppercase;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amounts {
|
.amounts {
|
||||||
display: flex;
|
display: grid;
|
||||||
width: 100%;
|
grid-template-columns: repeat(4, 1fr);
|
||||||
margin-bottom: 16px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sat-amount {
|
.sat-amount {
|
||||||
flex: 1 1 auto;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: inline-block;
|
|
||||||
background-color: #2a2a2a;
|
background-color: #2a2a2a;
|
||||||
color: var(--font-color);
|
color: var(--font-color);
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
@ -79,83 +51,28 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 17px;
|
line-height: 17px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .sat-amount {
|
.light .sat-amount {
|
||||||
background-color: var(--gray);
|
background-color: var(--gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sat-amount:not(:last-child) {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sat-amount:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sat-amount.active {
|
.sat-amount.active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--gray-superdark);
|
color: var(--gray-superdark);
|
||||||
background-color: var(--font-color);
|
background-color: var(--font-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lnurl-tip .invoice {
|
.lnurl-modal canvas {
|
||||||
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;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lnurl-tip .success-action .paid {
|
.lnurl-modal .success-action .paid {
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lnurl-tip .success-action a {
|
.lnurl-modal .success-action a {
|
||||||
color: var(--highlight);
|
color: var(--highlight);
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import "./SendSats.css";
|
import "./SendSats.css";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { ReactNode, useContext, useEffect, useState } from "react";
|
||||||
import { useIntl, FormattedMessage } from "react-intl";
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { HexKey, NostrEvent, EventPublisher } from "@snort/system";
|
import { HexKey } from "@snort/system";
|
||||||
import { LNURL, LNURLError, LNURLErrorCode, LNURLInvoice, LNURLSuccessAction } from "@snort/shared";
|
import { SnortContext } from "@snort/system-react";
|
||||||
|
import { LNURLSuccessAction } from "@snort/shared";
|
||||||
|
|
||||||
import { formatShort } from "Number";
|
import { formatShort } from "Number";
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
@ -12,12 +13,11 @@ import ProfileImage from "Element/ProfileImage";
|
|||||||
import Modal from "Element/Modal";
|
import Modal from "Element/Modal";
|
||||||
import QrCode from "Element/QrCode";
|
import QrCode from "Element/QrCode";
|
||||||
import Copy from "Element/Copy";
|
import Copy from "Element/Copy";
|
||||||
import { chunks, debounce } from "SnortUtils";
|
import { debounce } from "SnortUtils";
|
||||||
import { useWallet } from "Wallet";
|
import { LNWallet, useWallet } from "Wallet";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { generateRandomKey } from "Login";
|
|
||||||
import { ZapPoolController } from "ZapPoolController";
|
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
|
import { ZapTarget, Zapper } from "Zapper";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
@ -30,12 +30,11 @@ enum ZapType {
|
|||||||
|
|
||||||
export interface SendSatsProps {
|
export interface SendSatsProps {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
lnurl?: string;
|
targets?: Array<ZapTarget>;
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
invoice?: string; // shortcut to invoice qr tab
|
invoice?: string; // shortcut to invoice qr tab
|
||||||
title?: string;
|
title?: ReactNode;
|
||||||
notice?: string;
|
notice?: string;
|
||||||
target?: string;
|
|
||||||
note?: HexKey;
|
note?: HexKey;
|
||||||
author?: HexKey;
|
author?: HexKey;
|
||||||
allocatePool?: boolean;
|
allocatePool?: boolean;
|
||||||
@ -43,42 +42,21 @@ export interface SendSatsProps {
|
|||||||
|
|
||||||
export default function SendSats(props: SendSatsProps) {
|
export default function SendSats(props: SendSatsProps) {
|
||||||
const onClose = props.onClose || (() => undefined);
|
const onClose = props.onClose || (() => undefined);
|
||||||
const { note, author, target } = props;
|
|
||||||
const login = useLogin();
|
|
||||||
const defaultZapAmount = login.preferences.defaultZapAmount;
|
|
||||||
const amounts = [defaultZapAmount, 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 [handler, setHandler] = useState<LNURL>();
|
const [zapper, setZapper] = useState<Zapper>();
|
||||||
const [invoice, setInvoice] = useState<string>();
|
const [invoice, setInvoice] = useState<string>();
|
||||||
const [amount, setAmount] = useState<number>(defaultZapAmount);
|
|
||||||
const [customAmount, setCustomAmount] = useState<number>();
|
|
||||||
const [comment, setComment] = useState<string>();
|
|
||||||
const [success, setSuccess] = useState<LNURLSuccessAction>();
|
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const [zapType, setZapType] = useState(ZapType.PublicZap);
|
const [success, setSuccess] = useState<LNURLSuccessAction>();
|
||||||
const [paying, setPaying] = useState<boolean>(false);
|
const [amount, setAmount] = useState<SendSatsInputSelection>();
|
||||||
|
|
||||||
const { formatMessage } = useIntl();
|
const system = useContext(SnortContext);
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const canComment = handler ? (handler.canZap && zapType !== ZapType.NonZap) || handler.maxCommentLength > 0 : false;
|
|
||||||
const walletState = useWallet();
|
const walletState = useWallet();
|
||||||
const wallet = walletState.wallet;
|
const wallet = walletState.wallet;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.show) {
|
if (props.show) {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setAmount(defaultZapAmount);
|
|
||||||
setComment(undefined);
|
|
||||||
setZapType(ZapType.PublicZap);
|
|
||||||
setInvoice(props.invoice);
|
setInvoice(props.invoice);
|
||||||
setSuccess(undefined);
|
setSuccess(undefined);
|
||||||
}
|
}
|
||||||
@ -94,247 +72,30 @@ export default function SendSats(props: SendSatsProps) {
|
|||||||
}, [success]);
|
}, [success]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.lnurl && props.show) {
|
if (props.targets && props.show) {
|
||||||
try {
|
try {
|
||||||
const h = new LNURL(props.lnurl);
|
console.debug("loading zapper");
|
||||||
setHandler(h);
|
const zapper = new Zapper(system, publisher);
|
||||||
h.load().catch(e => handleLNURLError(e, formatMessage(messages.InvoiceFail)));
|
zapper.load(props.targets).then(() => {
|
||||||
|
console.debug(zapper);
|
||||||
|
setZapper(zapper);
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
setError(e.message);
|
setError(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [props.lnurl, props.show]);
|
}, [props.targets, props.show]);
|
||||||
|
|
||||||
const serviceAmounts = useMemo(() => {
|
|
||||||
if (handler) {
|
|
||||||
const min = handler.min / 1000;
|
|
||||||
const max = handler.max / 1000;
|
|
||||||
return amounts.filter(a => a >= min && a <= max);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}, [handler]);
|
|
||||||
const amountRows = useMemo(() => chunks(serviceAmounts, 3), [serviceAmounts]);
|
|
||||||
|
|
||||||
const selectAmount = (a: number) => {
|
|
||||||
setError(undefined);
|
|
||||||
setAmount(a);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function loadInvoice(): Promise<void> {
|
|
||||||
if (!amount || !handler || !publisher) return;
|
|
||||||
|
|
||||||
let zap: NostrEvent | undefined;
|
|
||||||
if (author && zapType !== ZapType.NonZap) {
|
|
||||||
const relays = Object.keys(login.relays.item);
|
|
||||||
|
|
||||||
// use random key for anon zaps
|
|
||||||
if (zapType === ZapType.AnonZap) {
|
|
||||||
const randomKey = generateRandomKey();
|
|
||||||
console.debug("Generated new key for zap: ", randomKey);
|
|
||||||
|
|
||||||
const publisher = EventPublisher.privateKey(randomKey.privateKey);
|
|
||||||
zap = await publisher.zap(amount * 1000, author, relays, note, comment, eb => eb.tag(["anon", ""]));
|
|
||||||
} else {
|
|
||||||
zap = await publisher.zap(amount * 1000, author, relays, note, comment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rsp = await handler.getInvoice(amount, comment, zap);
|
|
||||||
if (rsp.pr) {
|
|
||||||
setInvoice(rsp.pr);
|
|
||||||
await payWithWallet(rsp);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
handleLNURLError(e, formatMessage(messages.InvoiceFail));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLNURLError(e: unknown, fallback: string) {
|
|
||||||
if (e instanceof LNURLError) {
|
|
||||||
switch (e.code) {
|
|
||||||
case LNURLErrorCode.ServiceUnavailable: {
|
|
||||||
setError(formatMessage(messages.LNURLFail));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case LNURLErrorCode.InvalidLNURL: {
|
|
||||||
setError(formatMessage(messages.InvalidLNURL));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setError(fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function custom() {
|
|
||||||
if (!handler) return null;
|
|
||||||
const min = handler.min / 1000;
|
|
||||||
const max = handler.max / 1000;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="custom-amount flex">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
className="f-grow mr10"
|
|
||||||
placeholder={formatMessage(messages.Custom)}
|
|
||||||
value={customAmount}
|
|
||||||
onChange={e => setCustomAmount(parseInt(e.target.value))}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="secondary"
|
|
||||||
type="button"
|
|
||||||
disabled={!customAmount}
|
|
||||||
onClick={() => selectAmount(customAmount ?? 0)}>
|
|
||||||
<FormattedMessage {...messages.Confirm} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function payWithWallet(invoice: LNURLInvoice) {
|
|
||||||
try {
|
|
||||||
if (wallet?.isReady()) {
|
|
||||||
setPaying(true);
|
|
||||||
const res = await wallet.payInvoice(invoice?.pr ?? "");
|
|
||||||
if (props.allocatePool) {
|
|
||||||
ZapPoolController.allocate(amount);
|
|
||||||
}
|
|
||||||
console.log(res);
|
|
||||||
setSuccess(invoice?.successAction ?? {});
|
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
|
||||||
console.warn(e);
|
|
||||||
if (e instanceof Error) {
|
|
||||||
setError(e.toString());
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setPaying(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderAmounts(amount: number, amounts: number[]) {
|
|
||||||
return (
|
|
||||||
<div className="amounts">
|
|
||||||
{amounts.map(a => (
|
|
||||||
<span className={`sat-amount ${amount === a ? "active" : ""}`} key={a} onClick={() => selectAmount(a)}>
|
|
||||||
{emojis[a] && <>{emojis[a]} </>}
|
|
||||||
{a === 1000 ? "1K" : formatShort(a)}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function invoiceForm() {
|
|
||||||
if (!handler || invoice) return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3>
|
|
||||||
<FormattedMessage {...messages.ZapAmount} />
|
|
||||||
</h3>
|
|
||||||
{amountRows.map(amounts => renderAmounts(amount, amounts))}
|
|
||||||
{custom()}
|
|
||||||
<div className="flex">
|
|
||||||
{canComment && (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={formatMessage(messages.Comment)}
|
|
||||||
className="f-grow"
|
|
||||||
maxLength={handler.canZap && zapType !== ZapType.NonZap ? 250 : handler.maxCommentLength}
|
|
||||||
onChange={e => setComment(e.target.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{zapTypeSelector()}
|
|
||||||
{(amount ?? 0) > 0 && (
|
|
||||||
<AsyncButton className="zap-action" onClick={() => loadInvoice()}>
|
|
||||||
<div className="zap-action-container">
|
|
||||||
<Icon name="zap" />
|
|
||||||
{target ? (
|
|
||||||
<FormattedMessage {...messages.ZapTarget} values={{ target, n: formatShort(amount) }} />
|
|
||||||
) : (
|
|
||||||
<FormattedMessage {...messages.ZapSats} values={{ n: formatShort(amount) }} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</AsyncButton>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function zapTypeSelector() {
|
|
||||||
if (!handler || !handler.canZap) return;
|
|
||||||
|
|
||||||
const makeTab = (t: ZapType, n: React.ReactNode) => (
|
|
||||||
<div className={`tab${zapType === t ? " active" : ""}`} onClick={() => setZapType(t)}>
|
|
||||||
{n}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3>
|
|
||||||
<FormattedMessage defaultMessage="Zap Type" />
|
|
||||||
</h3>
|
|
||||||
<div className="tabs mt10">
|
|
||||||
{makeTab(ZapType.PublicZap, <FormattedMessage defaultMessage="Public" description="Public Zap" />)}
|
|
||||||
{/*makeTab(ZapType.PrivateZap, "Private")*/}
|
|
||||||
{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 || !invoice) return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="invoice">
|
|
||||||
{props.notice && <b className="error">{props.notice}</b>}
|
|
||||||
{paying ? (
|
|
||||||
<h4>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Paying with {wallet}"
|
|
||||||
values={{
|
|
||||||
wallet: walletState.config?.info.alias,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
...
|
|
||||||
</h4>
|
|
||||||
) : (
|
|
||||||
<QrCode data={invoice} link={`lightning:${invoice}`} />
|
|
||||||
)}
|
|
||||||
<div className="actions">
|
|
||||||
{invoice && (
|
|
||||||
<>
|
|
||||||
<div className="copy-action">
|
|
||||||
<Copy text={invoice} maxSize={26} />
|
|
||||||
</div>
|
|
||||||
<button className="wallet-action" type="button" onClick={() => window.open(`lightning:${invoice}`)}>
|
|
||||||
<FormattedMessage {...messages.OpenWallet} />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function successAction() {
|
function successAction() {
|
||||||
if (!success) return null;
|
if (!success) return null;
|
||||||
return (
|
return (
|
||||||
<div className="success-action">
|
<div className="flex f-center">
|
||||||
<p className="paid">
|
<p className="flex g12">
|
||||||
<Icon name="check" className="success mr10" />
|
<Icon name="check" className="success" />
|
||||||
{success?.description ?? <FormattedMessage {...messages.Paid} />}
|
{success?.description ?? <FormattedMessage defaultMessage="Paid" />}
|
||||||
</p>
|
</p>
|
||||||
{success.url && (
|
{success.url && (
|
||||||
<p>
|
<p>
|
||||||
@ -347,29 +108,318 @@ export default function SendSats(props: SendSatsProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultTitle = handler?.canZap ? formatMessage(messages.SendZap) : formatMessage(messages.SendSats);
|
function title() {
|
||||||
const title = target
|
if (!props.targets) {
|
||||||
? formatMessage(messages.ToTarget, {
|
return (
|
||||||
action: defaultTitle,
|
<>
|
||||||
target,
|
<h2>
|
||||||
})
|
{zapper?.canZap() ? (
|
||||||
: defaultTitle;
|
<FormattedMessage defaultMessage="Send zap" />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage defaultMessage="Send sats" />
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (props.targets.length === 1 && props.targets[0].name) {
|
||||||
|
const t = props.targets[0];
|
||||||
|
const values = {
|
||||||
|
name: t.name,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{t.zap?.pubkey && <ProfileImage pubkey={t.zap.pubkey} showUsername={false} />}
|
||||||
|
<h2>
|
||||||
|
{zapper?.canZap() ? (
|
||||||
|
<FormattedMessage defaultMessage="Send zap to {name}" values={values} />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage defaultMessage="Send sats to {name}" values={values} />
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (props.targets.length > 1) {
|
||||||
|
const total = props.targets.reduce((acc, v) => (acc += v.weight), 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-column g12">
|
||||||
|
<h2>
|
||||||
|
{zapper?.canZap() ? (
|
||||||
|
<FormattedMessage defaultMessage="Send zap splits to" />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage defaultMessage="Send sats splits to" />
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<div className="flex g4">
|
||||||
|
{props.targets.map(v => (
|
||||||
|
<ProfileImage
|
||||||
|
pubkey={v.value}
|
||||||
|
showUsername={false}
|
||||||
|
imageOverlay={formatShort(Math.floor((amount?.amount ?? 0) * (v.weight / total)))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!(props.show ?? false)) return null;
|
if (!(props.show ?? false)) return null;
|
||||||
return (
|
return (
|
||||||
<Modal className="lnurl-modal" onClose={onClose}>
|
<Modal className="lnurl-modal" onClose={onClose}>
|
||||||
<div className="lnurl-tip" onClick={e => e.stopPropagation()}>
|
<div className="p flex-column g12">
|
||||||
<div className="close" onClick={onClose}>
|
<div className="flex g12">
|
||||||
<Icon name="close" />
|
<div className="flex f-grow">{props.title || title()}</div>
|
||||||
|
<div onClick={onClose}>
|
||||||
|
<Icon name="close" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="lnurl-header">
|
{zapper && !invoice && (
|
||||||
{author && <ProfileImage pubkey={author} showUsername={false} />}
|
<SendSatsInput
|
||||||
<h2>{props.title || title}</h2>
|
zapper={zapper}
|
||||||
</div>
|
onChange={v => setAmount(v)}
|
||||||
{invoiceForm()}
|
onNextStage={async p => {
|
||||||
|
const targetsWithComments = (props.targets ?? []).map(v => {
|
||||||
|
if (p.comment) {
|
||||||
|
v.memo = p.comment;
|
||||||
|
}
|
||||||
|
if (p.type === ZapType.AnonZap && v.zap) {
|
||||||
|
v.zap = {
|
||||||
|
...v.zap,
|
||||||
|
anon: true,
|
||||||
|
};
|
||||||
|
} else if (p.type === ZapType.NonZap) {
|
||||||
|
v.zap = undefined;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
if (targetsWithComments.length > 0) {
|
||||||
|
const sends = await zapper.send(wallet, targetsWithComments, p.amount);
|
||||||
|
if (sends[0].error) {
|
||||||
|
setError(sends[0].error.message);
|
||||||
|
} else if (sends.length === 1) {
|
||||||
|
setInvoice(sends[0].pr);
|
||||||
|
} else if (sends.every(a => a.sent)) {
|
||||||
|
setSuccess({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
{payInvoice()}
|
{invoice && !success && (
|
||||||
|
<SendSatsInvoice
|
||||||
|
invoice={invoice}
|
||||||
|
wallet={wallet}
|
||||||
|
notice={props.notice}
|
||||||
|
onInvoicePaid={() => {
|
||||||
|
setSuccess({});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{successAction()}
|
{successAction()}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SendSatsInputSelection {
|
||||||
|
amount: number;
|
||||||
|
comment?: string;
|
||||||
|
type: ZapType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SendSatsInput(props: {
|
||||||
|
zapper: Zapper;
|
||||||
|
onChange?: (v: SendSatsInputSelection) => void;
|
||||||
|
onNextStage: (v: SendSatsInputSelection) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const login = useLogin();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const defaultZapAmount = login.preferences.defaultZapAmount;
|
||||||
|
const amounts: Record<string, string> = {
|
||||||
|
[defaultZapAmount.toString()]: "",
|
||||||
|
"1000": "👍",
|
||||||
|
"5000": "💜",
|
||||||
|
"10000": "😍",
|
||||||
|
"20000": "🤩",
|
||||||
|
"50000": "🔥",
|
||||||
|
"100000": "🚀",
|
||||||
|
"1000000": "🤯",
|
||||||
|
};
|
||||||
|
const [comment, setComment] = useState<string>();
|
||||||
|
const [amount, setAmount] = useState<number>(defaultZapAmount);
|
||||||
|
const [customAmount, setCustomAmount] = useState<number>(defaultZapAmount);
|
||||||
|
const [zapType, setZapType] = useState(ZapType.PublicZap);
|
||||||
|
|
||||||
|
function getValue() {
|
||||||
|
return {
|
||||||
|
amount,
|
||||||
|
comment,
|
||||||
|
type: zapType,
|
||||||
|
} as SendSatsInputSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(getValue());
|
||||||
|
}
|
||||||
|
}, [amount, comment, zapType]);
|
||||||
|
|
||||||
|
function renderAmounts() {
|
||||||
|
const min = props.zapper.minAmount() / 1000;
|
||||||
|
const max = props.zapper.maxAmount() / 1000;
|
||||||
|
const filteredAmounts = Object.entries(amounts).filter(([k]) => Number(k) >= min && Number(k) <= max);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="amounts">
|
||||||
|
{filteredAmounts.map(([k, v]) => (
|
||||||
|
<span
|
||||||
|
className={`sat-amount ${amount === Number(k) ? "active" : ""}`}
|
||||||
|
key={k}
|
||||||
|
onClick={() => setAmount(Number(k))}>
|
||||||
|
{v}
|
||||||
|
{k === "1000" ? "1K" : formatShort(Number(k))}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function custom() {
|
||||||
|
const min = props.zapper.minAmount() / 1000;
|
||||||
|
const max = props.zapper.maxAmount() / 1000;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex g8">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
className="f-grow"
|
||||||
|
placeholder={formatMessage(messages.Custom)}
|
||||||
|
value={customAmount}
|
||||||
|
onChange={e => setCustomAmount(parseInt(e.target.value))}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="secondary"
|
||||||
|
type="button"
|
||||||
|
disabled={!customAmount}
|
||||||
|
onClick={() => setAmount(customAmount ?? 0)}>
|
||||||
|
<FormattedMessage {...messages.Confirm} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-column g24">
|
||||||
|
<div className="flex-column g8">
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage defaultMessage="Zap amount in sats" />
|
||||||
|
</h3>
|
||||||
|
{renderAmounts()}
|
||||||
|
{custom()}
|
||||||
|
{props.zapper.maxComment() > 0 && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={formatMessage(messages.Comment)}
|
||||||
|
className="f-grow"
|
||||||
|
maxLength={props.zapper.maxComment()}
|
||||||
|
onChange={e => setComment(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<SendSatsZapTypeSelector zapType={zapType} setZapType={setZapType} />
|
||||||
|
{(amount ?? 0) > 0 && (
|
||||||
|
<AsyncButton className="zap-action" onClick={() => props.onNextStage(getValue())}>
|
||||||
|
<div className="zap-action-container">
|
||||||
|
<Icon name="zap" />
|
||||||
|
<FormattedMessage defaultMessage="Zap {n} sats" values={{ n: formatShort(amount) }} />
|
||||||
|
</div>
|
||||||
|
</AsyncButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SendSatsZapTypeSelector({ zapType, setZapType }: { zapType: ZapType; setZapType: (t: ZapType) => void }) {
|
||||||
|
const makeTab = (t: ZapType, n: React.ReactNode) => (
|
||||||
|
<button type="button" className={zapType === t ? "" : "secondary"} onClick={() => setZapType(t)}>
|
||||||
|
{n}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="flex-column g8">
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage defaultMessage="Zap Type" />
|
||||||
|
</h3>
|
||||||
|
<div className="flex g8">
|
||||||
|
{makeTab(ZapType.PublicZap, <FormattedMessage defaultMessage="Public" description="Public Zap" />)}
|
||||||
|
{/*makeTab(ZapType.PrivateZap, "Private")*/}
|
||||||
|
{makeTab(ZapType.AnonZap, <FormattedMessage defaultMessage="Anon" description="Anonymous Zap" />)}
|
||||||
|
{makeTab(
|
||||||
|
ZapType.NonZap,
|
||||||
|
<FormattedMessage defaultMessage="Non-Zap" description="Non-Zap, Regular LN payment" />,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SendSatsInvoice(props: { invoice: string; wallet?: LNWallet; notice?: ReactNode; onInvoicePaid: () => void }) {
|
||||||
|
const [paying, setPaying] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
async function payWithWallet() {
|
||||||
|
try {
|
||||||
|
if (props.wallet?.isReady()) {
|
||||||
|
setPaying(true);
|
||||||
|
const res = await props.wallet.payInvoice(props.invoice);
|
||||||
|
console.log(res);
|
||||||
|
props.onInvoicePaid();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
setError(e.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setPaying(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.wallet && !paying && !error) {
|
||||||
|
payWithWallet();
|
||||||
|
}
|
||||||
|
}, [props.wallet, props.invoice, paying]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-column g12 txt-center">
|
||||||
|
{error && <p className="error">{error}</p>}
|
||||||
|
{props.notice && <b className="error">{props.notice}</b>}
|
||||||
|
{paying ? (
|
||||||
|
<h4>
|
||||||
|
<FormattedMessage defaultMessage="Paying with wallet" />
|
||||||
|
...
|
||||||
|
</h4>
|
||||||
|
) : (
|
||||||
|
<QrCode data={props.invoice} link={`lightning:${props.invoice}`} />
|
||||||
|
)}
|
||||||
|
<div className="flex-column g12">
|
||||||
|
{props.invoice && (
|
||||||
|
<>
|
||||||
|
<Copy text={props.invoice} maxSize={26} className="f-center" />
|
||||||
|
<a href={`lightning:${props.invoice}`}>
|
||||||
|
<button type="button">
|
||||||
|
<FormattedMessage defaultMessage="Open Wallet" />
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 16px 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs::-webkit-scrollbar {
|
.tabs::-webkit-scrollbar {
|
||||||
|
@ -31,7 +31,7 @@ export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
|
|||||||
const Tabs = ({ tabs, tab, setTab }: TabsProps) => {
|
const Tabs = ({ tabs, tab, setTab }: TabsProps) => {
|
||||||
const horizontalScroll = useHorizontalScroll();
|
const horizontalScroll = useHorizontalScroll();
|
||||||
return (
|
return (
|
||||||
<div className="tabs" ref={horizontalScroll}>
|
<div className="tabs p" ref={horizontalScroll}>
|
||||||
{tabs.map(t => (
|
{tabs.map(t => (
|
||||||
<TabElement tab={tab} setTab={setTab} t={t} />
|
<TabElement tab={tab} setTab={setTab} t={t} />
|
||||||
))}
|
))}
|
||||||
|
@ -29,8 +29,7 @@ const ZapButton = ({
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<SendSats
|
<SendSats
|
||||||
target={profile?.display_name || profile?.name}
|
targets={[{ type: "lnurl", value: service, weight: 1, name: profile?.display_name || profile?.name }]}
|
||||||
lnurl={service}
|
|
||||||
show={zap}
|
show={zap}
|
||||||
onClose={() => setZap(false)}
|
onClose={() => setZap(false)}
|
||||||
author={pubkey}
|
author={pubkey}
|
||||||
|
@ -29,7 +29,6 @@ export default defineMessages({
|
|||||||
PayInvoice: { defaultMessage: "Pay Invoice" },
|
PayInvoice: { defaultMessage: "Pay Invoice" },
|
||||||
Expired: { defaultMessage: "Expired" },
|
Expired: { defaultMessage: "Expired" },
|
||||||
Pay: { defaultMessage: "Pay" },
|
Pay: { defaultMessage: "Pay" },
|
||||||
Paid: { defaultMessage: "Paid" },
|
|
||||||
Loading: { defaultMessage: "Loading..." },
|
Loading: { defaultMessage: "Loading..." },
|
||||||
Logout: { defaultMessage: "Logout" },
|
Logout: { defaultMessage: "Logout" },
|
||||||
ShowMore: { defaultMessage: "Show more" },
|
ShowMore: { defaultMessage: "Show more" },
|
||||||
@ -64,14 +63,8 @@ export default defineMessages({
|
|||||||
InvoiceFail: { defaultMessage: "Failed to load invoice" },
|
InvoiceFail: { defaultMessage: "Failed to load invoice" },
|
||||||
Custom: { defaultMessage: "Custom" },
|
Custom: { defaultMessage: "Custom" },
|
||||||
Confirm: { defaultMessage: "Confirm" },
|
Confirm: { defaultMessage: "Confirm" },
|
||||||
ZapAmount: { defaultMessage: "Zap amount in sats" },
|
|
||||||
Comment: { defaultMessage: "Comment" },
|
Comment: { defaultMessage: "Comment" },
|
||||||
ZapTarget: { defaultMessage: "Zap {target} {n} sats" },
|
|
||||||
ZapSats: { defaultMessage: "Zap {n} sats" },
|
|
||||||
OpenWallet: { defaultMessage: "Open Wallet" },
|
|
||||||
SendZap: { defaultMessage: "Send zap" },
|
SendZap: { defaultMessage: "Send zap" },
|
||||||
SendSats: { defaultMessage: "Send sats" },
|
|
||||||
ToTarget: { defaultMessage: "{action} to {target}" },
|
|
||||||
ShowReplies: { defaultMessage: "Show replies" },
|
ShowReplies: { defaultMessage: "Show replies" },
|
||||||
TooShort: { defaultMessage: "name too short" },
|
TooShort: { defaultMessage: "name too short" },
|
||||||
TooLong: { defaultMessage: "name too long" },
|
TooLong: { defaultMessage: "name too long" },
|
||||||
|
@ -29,7 +29,7 @@ export default function Discover() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="tabs">
|
<div className="tabs p">
|
||||||
{[Tabs.Follows, Tabs.Posts, Tabs.Profiles].map(a => (
|
{[Tabs.Follows, Tabs.Posts, Tabs.Profiles].map(a => (
|
||||||
<TabElement key={a.value} tab={tab} setTab={setTab} t={a} />
|
<TabElement key={a.value} tab={tab} setTab={setTab} t={a} />
|
||||||
))}
|
))}
|
||||||
|
@ -47,6 +47,8 @@ const Translators = [
|
|||||||
bech32ToHex("npub1z9n5ktfjrlpyywds9t7ljekr9cm9jjnzs27h702te5fy8p2c4dgs5zvycf"), // Felix - DE
|
bech32ToHex("npub1z9n5ktfjrlpyywds9t7ljekr9cm9jjnzs27h702te5fy8p2c4dgs5zvycf"), // Felix - DE
|
||||||
|
|
||||||
bech32ToHex("npub1wh30wunfpkezx5s7edqu9g0s0raeetf5dgthzm0zw7sk8wqygmjqqfljgh"), // Fernando Porazzi - pt-BR
|
bech32ToHex("npub1wh30wunfpkezx5s7edqu9g0s0raeetf5dgthzm0zw7sk8wqygmjqqfljgh"), // Fernando Porazzi - pt-BR
|
||||||
|
|
||||||
|
bech32ToHex("npub1ust7u0v3qffejwhqee45r49zgcyewrcn99vdwkednd356c9resyqtnn3mj"), // Petri - FI
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DonateLNURL = "donate@snort.social";
|
export const DonateLNURL = "donate@snort.social";
|
||||||
|
@ -291,11 +291,14 @@ export default function ProfilePage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<SendSats
|
<SendSats
|
||||||
lnurl={lnurl?.lnurl}
|
targets={
|
||||||
|
lnurl?.lnurl
|
||||||
|
? [{ type: "lnurl", value: lnurl?.lnurl, weight: 1, name: user?.display_name || user?.name }]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
show={showLnQr}
|
show={showLnQr}
|
||||||
onClose={() => setShowLnQr(false)}
|
onClose={() => setShowLnQr(false)}
|
||||||
author={id}
|
author={id}
|
||||||
target={user?.display_name || user?.name}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -471,7 +474,7 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<div className="tabs" ref={horizontalScroll}>
|
<div className="tabs p" ref={horizontalScroll}>
|
||||||
{[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows].map(renderTab)}
|
{[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows].map(renderTab)}
|
||||||
{optionalTabs.map(renderTab)}
|
{optionalTabs.map(renderTab)}
|
||||||
{isMe && blocked.length > 0 && renderTab(ProfileTab.Blocked)}
|
{isMe && blocked.length > 0 && renderTab(ProfileTab.Blocked)}
|
||||||
|
@ -106,7 +106,7 @@ const SearchPage = () => {
|
|||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="tabs">{[SearchTab.Posts, SearchTab.Profiles].map(renderTab)}</div>
|
<div className="tabs p">{[SearchTab.Posts, SearchTab.Profiles].map(renderTab)}</div>
|
||||||
{tabContent()}
|
{tabContent()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { NostrEvent, TaggedNostrEvent } from "@snort/system";
|
import { NostrEvent, TaggedNostrEvent } from "@snort/system";
|
||||||
|
import { ZapTarget } from "Zapper";
|
||||||
|
|
||||||
interface NoteCreatorStore {
|
interface NoteCreatorStore {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@ -10,7 +11,7 @@ interface NoteCreatorStore {
|
|||||||
replyTo?: TaggedNostrEvent;
|
replyTo?: TaggedNostrEvent;
|
||||||
showAdvanced: boolean;
|
showAdvanced: boolean;
|
||||||
selectedCustomRelays: false | Array<string>;
|
selectedCustomRelays: false | Array<string>;
|
||||||
zapForward: string;
|
zapSplits?: Array<ZapTarget>;
|
||||||
sensitive: string;
|
sensitive: string;
|
||||||
pollOptions?: Array<string>;
|
pollOptions?: Array<string>;
|
||||||
otherEvents: Array<NostrEvent>;
|
otherEvents: Array<NostrEvent>;
|
||||||
@ -23,7 +24,6 @@ const InitState: NoteCreatorStore = {
|
|||||||
active: false,
|
active: false,
|
||||||
showAdvanced: false,
|
showAdvanced: false,
|
||||||
selectedCustomRelays: false,
|
selectedCustomRelays: false,
|
||||||
zapForward: "",
|
|
||||||
sensitive: "",
|
sensitive: "",
|
||||||
otherEvents: [],
|
otherEvents: [],
|
||||||
};
|
};
|
||||||
@ -56,9 +56,6 @@ const NoteCreatorSlice = createSlice({
|
|||||||
setSelectedCustomRelays: (state, action: PayloadAction<false | Array<string>>) => {
|
setSelectedCustomRelays: (state, action: PayloadAction<false | Array<string>>) => {
|
||||||
state.selectedCustomRelays = action.payload;
|
state.selectedCustomRelays = action.payload;
|
||||||
},
|
},
|
||||||
setZapForward: (state, action: PayloadAction<string>) => {
|
|
||||||
state.zapForward = action.payload;
|
|
||||||
},
|
|
||||||
setSensitive: (state, action: PayloadAction<string>) => {
|
setSensitive: (state, action: PayloadAction<string>) => {
|
||||||
state.sensitive = action.payload;
|
state.sensitive = action.payload;
|
||||||
},
|
},
|
||||||
@ -68,6 +65,9 @@ const NoteCreatorSlice = createSlice({
|
|||||||
setOtherEvents: (state, action: PayloadAction<Array<NostrEvent>>) => {
|
setOtherEvents: (state, action: PayloadAction<Array<NostrEvent>>) => {
|
||||||
state.otherEvents = action.payload;
|
state.otherEvents = action.payload;
|
||||||
},
|
},
|
||||||
|
setZapSplits: (state, action: PayloadAction<Array<ZapTarget>>) => {
|
||||||
|
state.zapSplits = action.payload;
|
||||||
|
},
|
||||||
reset: () => InitState,
|
reset: () => InitState,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -81,7 +81,7 @@ export const {
|
|||||||
setReplyTo,
|
setReplyTo,
|
||||||
setShowAdvanced,
|
setShowAdvanced,
|
||||||
setSelectedCustomRelays,
|
setSelectedCustomRelays,
|
||||||
setZapForward,
|
setZapSplits,
|
||||||
setSensitive,
|
setSensitive,
|
||||||
setPollOptions,
|
setPollOptions,
|
||||||
setOtherEvents,
|
setOtherEvents,
|
||||||
|
208
packages/app/src/Zapper.ts
Normal file
208
packages/app/src/Zapper.ts
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import { LNURL } from "@snort/shared";
|
||||||
|
import {
|
||||||
|
EventPublisher,
|
||||||
|
NostrEvent,
|
||||||
|
NostrLink,
|
||||||
|
SystemInterface,
|
||||||
|
createNostrLinkToEvent,
|
||||||
|
linkToEventTag,
|
||||||
|
} from "@snort/system";
|
||||||
|
import { generateRandomKey } from "Login";
|
||||||
|
import { isHex } from "SnortUtils";
|
||||||
|
import { LNWallet, WalletInvoiceState } from "Wallet";
|
||||||
|
|
||||||
|
export interface ZapTarget {
|
||||||
|
type: "lnurl" | "pubkey";
|
||||||
|
value: string;
|
||||||
|
weight: number;
|
||||||
|
memo?: string;
|
||||||
|
name?: string;
|
||||||
|
zap?: {
|
||||||
|
pubkey: string;
|
||||||
|
anon: boolean;
|
||||||
|
event?: NostrLink;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZapTargetResult {
|
||||||
|
target: ZapTarget;
|
||||||
|
paid: boolean;
|
||||||
|
sent: number;
|
||||||
|
fee: number;
|
||||||
|
pr: string;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ZapTargetLoaded {
|
||||||
|
target: ZapTarget;
|
||||||
|
svc?: LNURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Zapper {
|
||||||
|
#inProgress = false;
|
||||||
|
#loadedTargets?: Array<ZapTargetLoaded>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly system: SystemInterface,
|
||||||
|
readonly publisher?: EventPublisher,
|
||||||
|
readonly onResult?: (r: ZapTargetResult) => void,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create targets from Event
|
||||||
|
*/
|
||||||
|
static fromEvent(ev: NostrEvent) {
|
||||||
|
return ev.tags
|
||||||
|
.filter(a => a[0] === "zap")
|
||||||
|
.map(v => {
|
||||||
|
if (v[1].length === 64 && isHex(v[1]) && v.length === 4) {
|
||||||
|
// NIP-57.G
|
||||||
|
return {
|
||||||
|
type: "pubkey",
|
||||||
|
value: v[1],
|
||||||
|
weight: Number(v[3] ?? 0),
|
||||||
|
zap: {
|
||||||
|
pubkey: v[1],
|
||||||
|
event: createNostrLinkToEvent(ev),
|
||||||
|
},
|
||||||
|
} as ZapTarget;
|
||||||
|
} else {
|
||||||
|
// assume event specific zap target
|
||||||
|
return {
|
||||||
|
type: "lnurl",
|
||||||
|
value: v[1],
|
||||||
|
weight: 1,
|
||||||
|
zap: {
|
||||||
|
pubkey: ev.pubkey,
|
||||||
|
event: createNostrLinkToEvent(ev),
|
||||||
|
},
|
||||||
|
} as ZapTarget;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(wallet: LNWallet | undefined, targets: Array<ZapTarget>, amount: number) {
|
||||||
|
if (this.#inProgress) {
|
||||||
|
throw new Error("Payout already in progress");
|
||||||
|
}
|
||||||
|
this.#inProgress = true;
|
||||||
|
|
||||||
|
const total = targets.reduce((acc, v) => (acc += v.weight), 0);
|
||||||
|
const ret = [] as Array<ZapTargetResult>;
|
||||||
|
|
||||||
|
for (const t of targets) {
|
||||||
|
const toSend = Math.floor(amount * (t.weight / total));
|
||||||
|
try {
|
||||||
|
const svc = await this.#getService(t);
|
||||||
|
if (!svc) {
|
||||||
|
throw new Error(`Failed to get invoice from ${t.value}`);
|
||||||
|
}
|
||||||
|
const relays = this.system.Sockets.filter(a => !a.ephemeral).map(v => v.address);
|
||||||
|
const pub = t.zap?.anon ?? false ? EventPublisher.privateKey(generateRandomKey().privateKey) : this.publisher;
|
||||||
|
const zap =
|
||||||
|
t.zap && svc.canZap
|
||||||
|
? await pub?.zap(toSend * 1000, t.zap.pubkey, relays, undefined, t.memo, eb => {
|
||||||
|
if (t.zap?.event) {
|
||||||
|
const tag = linkToEventTag(t.zap.event);
|
||||||
|
if (tag) {
|
||||||
|
eb.tag(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (t.zap?.anon) {
|
||||||
|
eb.tag(["anon", ""]);
|
||||||
|
}
|
||||||
|
return eb;
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
const invoice = await svc.getInvoice(toSend, t.memo, zap);
|
||||||
|
if (invoice?.pr) {
|
||||||
|
const res = await wallet?.payInvoice(invoice.pr);
|
||||||
|
ret.push({
|
||||||
|
target: t,
|
||||||
|
paid: res?.state === WalletInvoiceState.Paid,
|
||||||
|
sent: toSend,
|
||||||
|
pr: invoice.pr,
|
||||||
|
fee: res?.fees ?? 0,
|
||||||
|
});
|
||||||
|
this.onResult?.(ret[ret.length - 1]);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Failed to get invoice from ${t.value}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ret.push({
|
||||||
|
target: t,
|
||||||
|
paid: false,
|
||||||
|
sent: 0,
|
||||||
|
fee: 0,
|
||||||
|
pr: "",
|
||||||
|
error: e as Error,
|
||||||
|
});
|
||||||
|
this.onResult?.(ret[ret.length - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#inProgress = false;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(targets: Array<ZapTarget>) {
|
||||||
|
const svcs = targets.map(async a => {
|
||||||
|
return {
|
||||||
|
target: a,
|
||||||
|
loading: await this.#getService(a),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const loaded = await Promise.all(svcs);
|
||||||
|
this.#loadedTargets = loaded.map(a => ({
|
||||||
|
target: a.target,
|
||||||
|
svc: a.loading,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any target supports zaps
|
||||||
|
*/
|
||||||
|
canZap() {
|
||||||
|
return this.#loadedTargets?.some(a => a.svc?.canZap ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max comment length which can be sent to all (smallest comment length)
|
||||||
|
*/
|
||||||
|
maxComment() {
|
||||||
|
return (
|
||||||
|
this.#loadedTargets
|
||||||
|
?.map(a => (a.svc?.canZap ? 255 : a.svc?.maxCommentLength ?? 0))
|
||||||
|
.reduce((acc, v) => (acc > v ? v : acc), 255) ?? 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max of the min amounts
|
||||||
|
*/
|
||||||
|
minAmount() {
|
||||||
|
return this.#loadedTargets?.map(a => a.svc?.min ?? 0).reduce((acc, v) => (acc < v ? v : acc), 1000) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Min of the max amounts
|
||||||
|
*/
|
||||||
|
maxAmount() {
|
||||||
|
return this.#loadedTargets?.map(a => a.svc?.max ?? 100e9).reduce((acc, v) => (acc > v ? v : acc), 100e9) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #getService(t: ZapTarget) {
|
||||||
|
if (t.type === "lnurl") {
|
||||||
|
const svc = new LNURL(t.value);
|
||||||
|
await svc.load();
|
||||||
|
return svc;
|
||||||
|
} else if (t.type === "pubkey") {
|
||||||
|
const profile = await this.system.ProfileLoader.fetchProfile(t.value);
|
||||||
|
if (profile) {
|
||||||
|
const svc = new LNURL(profile.lud16 ?? profile.lud06 ?? "");
|
||||||
|
await svc.load();
|
||||||
|
return svc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -363,6 +363,14 @@ input:disabled {
|
|||||||
flex: 2;
|
flex: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.f-3 {
|
||||||
|
flex: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f-4 {
|
||||||
|
flex: 4;
|
||||||
|
}
|
||||||
|
|
||||||
.f-grow {
|
.f-grow {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@ -421,6 +429,10 @@ input:disabled {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.txt-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.w-max {
|
.w-max {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
width: stretch;
|
width: stretch;
|
||||||
|
@ -36,6 +36,9 @@
|
|||||||
"/RD0e2": {
|
"/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."
|
"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."
|
||||||
},
|
},
|
||||||
|
"/Xf4UW": {
|
||||||
|
"defaultMessage": "Send anonymous usage metrics"
|
||||||
|
},
|
||||||
"/d6vEc": {
|
"/d6vEc": {
|
||||||
"defaultMessage": "Make your profile easier to find and share"
|
"defaultMessage": "Make your profile easier to find and share"
|
||||||
},
|
},
|
||||||
@ -157,6 +160,9 @@
|
|||||||
"5BVs2e": {
|
"5BVs2e": {
|
||||||
"defaultMessage": "zap"
|
"defaultMessage": "zap"
|
||||||
},
|
},
|
||||||
|
"5CB6zB": {
|
||||||
|
"defaultMessage": "Zap Splits"
|
||||||
|
},
|
||||||
"5JcXdV": {
|
"5JcXdV": {
|
||||||
"defaultMessage": "Create Account"
|
"defaultMessage": "Create Account"
|
||||||
},
|
},
|
||||||
@ -181,6 +187,9 @@
|
|||||||
"6Yfvvp": {
|
"6Yfvvp": {
|
||||||
"defaultMessage": "Get an identifier"
|
"defaultMessage": "Get an identifier"
|
||||||
},
|
},
|
||||||
|
"6bgpn+": {
|
||||||
|
"defaultMessage": "Not all clients support this, you may still receive some zaps as if zap splits was not configured"
|
||||||
|
},
|
||||||
"6ewQqw": {
|
"6ewQqw": {
|
||||||
"defaultMessage": "Likes ({n})"
|
"defaultMessage": "Likes ({n})"
|
||||||
},
|
},
|
||||||
@ -196,9 +205,6 @@
|
|||||||
"7hp70g": {
|
"7hp70g": {
|
||||||
"defaultMessage": "NIP-05"
|
"defaultMessage": "NIP-05"
|
||||||
},
|
},
|
||||||
"7xzTiH": {
|
|
||||||
"defaultMessage": "{action} to {target}"
|
|
||||||
},
|
|
||||||
"8/vBbP": {
|
"8/vBbP": {
|
||||||
"defaultMessage": "Reposts ({n})"
|
"defaultMessage": "Reposts ({n})"
|
||||||
},
|
},
|
||||||
@ -208,6 +214,12 @@
|
|||||||
"8QDesP": {
|
"8QDesP": {
|
||||||
"defaultMessage": "Zap {n} sats"
|
"defaultMessage": "Zap {n} sats"
|
||||||
},
|
},
|
||||||
|
"8Rkoyb": {
|
||||||
|
"defaultMessage": "Recipient"
|
||||||
|
},
|
||||||
|
"8Y6bZQ": {
|
||||||
|
"defaultMessage": "Invalid zap split: {input}"
|
||||||
|
},
|
||||||
"8g2vyB": {
|
"8g2vyB": {
|
||||||
"defaultMessage": "name too long"
|
"defaultMessage": "name too long"
|
||||||
},
|
},
|
||||||
@ -217,6 +229,9 @@
|
|||||||
"9+Ddtu": {
|
"9+Ddtu": {
|
||||||
"defaultMessage": "Next"
|
"defaultMessage": "Next"
|
||||||
},
|
},
|
||||||
|
"91VPqq": {
|
||||||
|
"defaultMessage": "Paying with wallet"
|
||||||
|
},
|
||||||
"9HU8vw": {
|
"9HU8vw": {
|
||||||
"defaultMessage": "Reply"
|
"defaultMessage": "Reply"
|
||||||
},
|
},
|
||||||
@ -367,9 +382,6 @@
|
|||||||
"FDguSC": {
|
"FDguSC": {
|
||||||
"defaultMessage": "{n} Zaps"
|
"defaultMessage": "{n} Zaps"
|
||||||
},
|
},
|
||||||
"FP+D3H": {
|
|
||||||
"defaultMessage": "LNURL to forward zaps to"
|
|
||||||
},
|
|
||||||
"FS3b54": {
|
"FS3b54": {
|
||||||
"defaultMessage": "Done!"
|
"defaultMessage": "Done!"
|
||||||
},
|
},
|
||||||
@ -464,6 +476,9 @@
|
|||||||
"JCIgkj": {
|
"JCIgkj": {
|
||||||
"defaultMessage": "Username"
|
"defaultMessage": "Username"
|
||||||
},
|
},
|
||||||
|
"JGrt9q": {
|
||||||
|
"defaultMessage": "Send sats to {name}"
|
||||||
|
},
|
||||||
"JHEHCk": {
|
"JHEHCk": {
|
||||||
"defaultMessage": "Zaps ({n})"
|
"defaultMessage": "Zaps ({n})"
|
||||||
},
|
},
|
||||||
@ -521,6 +536,9 @@
|
|||||||
"Lw+I+J": {
|
"Lw+I+J": {
|
||||||
"defaultMessage": "{n,plural,=0{{name} zapped} other{{name} & {n} others zapped}}"
|
"defaultMessage": "{n,plural,=0{{name} zapped} other{{name} & {n} others zapped}}"
|
||||||
},
|
},
|
||||||
|
"LwYmVi": {
|
||||||
|
"defaultMessage": "Zaps on this note will be split to the following users."
|
||||||
|
},
|
||||||
"M3Oirc": {
|
"M3Oirc": {
|
||||||
"defaultMessage": "Debug Menus"
|
"defaultMessage": "Debug Menus"
|
||||||
},
|
},
|
||||||
@ -588,9 +606,6 @@
|
|||||||
"ORGv1Q": {
|
"ORGv1Q": {
|
||||||
"defaultMessage": "Created"
|
"defaultMessage": "Created"
|
||||||
},
|
},
|
||||||
"P04gQm": {
|
|
||||||
"defaultMessage": "All zaps sent to this note will be received by the following LNURL"
|
|
||||||
},
|
|
||||||
"P61BTu": {
|
"P61BTu": {
|
||||||
"defaultMessage": "Copy Event JSON"
|
"defaultMessage": "Copy Event JSON"
|
||||||
},
|
},
|
||||||
@ -634,9 +649,6 @@
|
|||||||
"R/6nsx": {
|
"R/6nsx": {
|
||||||
"defaultMessage": "Subscription"
|
"defaultMessage": "Subscription"
|
||||||
},
|
},
|
||||||
"R1fEdZ": {
|
|
||||||
"defaultMessage": "Forward Zaps"
|
|
||||||
},
|
|
||||||
"R81upa": {
|
"R81upa": {
|
||||||
"defaultMessage": "People you follow"
|
"defaultMessage": "People you follow"
|
||||||
},
|
},
|
||||||
@ -666,6 +678,9 @@
|
|||||||
"defaultMessage": "Sort",
|
"defaultMessage": "Sort",
|
||||||
"description": "Label for sorting options for people search"
|
"description": "Label for sorting options for people search"
|
||||||
},
|
},
|
||||||
|
"SMO+on": {
|
||||||
|
"defaultMessage": "Send zap to {name}"
|
||||||
|
},
|
||||||
"SOqbe9": {
|
"SOqbe9": {
|
||||||
"defaultMessage": "Update Lightning Address"
|
"defaultMessage": "Update Lightning Address"
|
||||||
},
|
},
|
||||||
@ -756,12 +771,18 @@
|
|||||||
"WONP5O": {
|
"WONP5O": {
|
||||||
"defaultMessage": "Find your twitter follows on nostr (Data provided by {provider})"
|
"defaultMessage": "Find your twitter follows on nostr (Data provided by {provider})"
|
||||||
},
|
},
|
||||||
|
"WvGmZT": {
|
||||||
|
"defaultMessage": "npub / nprofile / nostr address"
|
||||||
|
},
|
||||||
"WxthCV": {
|
"WxthCV": {
|
||||||
"defaultMessage": "e.g. Jack"
|
"defaultMessage": "e.g. Jack"
|
||||||
},
|
},
|
||||||
"X7xU8J": {
|
"X7xU8J": {
|
||||||
"defaultMessage": "nsec, npub, nip-05, hex, mnemonic"
|
"defaultMessage": "nsec, npub, nip-05, hex, mnemonic"
|
||||||
},
|
},
|
||||||
|
"XECMfW": {
|
||||||
|
"defaultMessage": "Send usage metrics"
|
||||||
|
},
|
||||||
"XICsE8": {
|
"XICsE8": {
|
||||||
"defaultMessage": "File hosts"
|
"defaultMessage": "File hosts"
|
||||||
},
|
},
|
||||||
@ -799,6 +820,9 @@
|
|||||||
"ZLmyG9": {
|
"ZLmyG9": {
|
||||||
"defaultMessage": "Contributors"
|
"defaultMessage": "Contributors"
|
||||||
},
|
},
|
||||||
|
"ZS+jRE": {
|
||||||
|
"defaultMessage": "Send zap splits to"
|
||||||
|
},
|
||||||
"ZUZedV": {
|
"ZUZedV": {
|
||||||
"defaultMessage": "Lightning Donation:"
|
"defaultMessage": "Lightning Donation:"
|
||||||
},
|
},
|
||||||
@ -1209,6 +1233,9 @@
|
|||||||
"sWnYKw": {
|
"sWnYKw": {
|
||||||
"defaultMessage": "Snort is designed to have a similar experience to Twitter."
|
"defaultMessage": "Snort is designed to have a similar experience to Twitter."
|
||||||
},
|
},
|
||||||
|
"sZQzjQ": {
|
||||||
|
"defaultMessage": "Failed to parse zap split: {input}"
|
||||||
|
},
|
||||||
"svOoEH": {
|
"svOoEH": {
|
||||||
"defaultMessage": "Name-squatting and impersonation is not allowed. Snort and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule."
|
"defaultMessage": "Name-squatting and impersonation is not allowed. Snort and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule."
|
||||||
},
|
},
|
||||||
@ -1230,12 +1257,12 @@
|
|||||||
"u4bHcR": {
|
"u4bHcR": {
|
||||||
"defaultMessage": "Check out the code here: {link}"
|
"defaultMessage": "Check out the code here: {link}"
|
||||||
},
|
},
|
||||||
"uD/N6c": {
|
|
||||||
"defaultMessage": "Zap {target} {n} sats"
|
|
||||||
},
|
|
||||||
"uSV4Ti": {
|
"uSV4Ti": {
|
||||||
"defaultMessage": "Reposts need to be manually confirmed"
|
"defaultMessage": "Reposts need to be manually confirmed"
|
||||||
},
|
},
|
||||||
|
"uc0din": {
|
||||||
|
"defaultMessage": "Send sats splits to"
|
||||||
|
},
|
||||||
"usAvMr": {
|
"usAvMr": {
|
||||||
"defaultMessage": "Edit Profile"
|
"defaultMessage": "Edit Profile"
|
||||||
},
|
},
|
||||||
@ -1248,9 +1275,6 @@
|
|||||||
"vOKedj": {
|
"vOKedj": {
|
||||||
"defaultMessage": "{n,plural,=1{& {n} other} other{& {n} others}}"
|
"defaultMessage": "{n,plural,=1{& {n} other} other{& {n} others}}"
|
||||||
},
|
},
|
||||||
"vU71Ez": {
|
|
||||||
"defaultMessage": "Paying with {wallet}"
|
|
||||||
},
|
|
||||||
"vZ4quW": {
|
"vZ4quW": {
|
||||||
"defaultMessage": "NIP-05 is a DNS based verification spec which helps to validate you as a real user."
|
"defaultMessage": "NIP-05 is a DNS based verification spec which helps to validate you as a real user."
|
||||||
},
|
},
|
||||||
@ -1336,6 +1360,9 @@
|
|||||||
"defaultMessage": "Read global from",
|
"defaultMessage": "Read global from",
|
||||||
"description": "Label for reading global feed from specific relays"
|
"description": "Label for reading global feed from specific relays"
|
||||||
},
|
},
|
||||||
|
"zCb8fX": {
|
||||||
|
"defaultMessage": "Weight"
|
||||||
|
},
|
||||||
"zFegDD": {
|
"zFegDD": {
|
||||||
"defaultMessage": "Contact"
|
"defaultMessage": "Contact"
|
||||||
},
|
},
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"/JE/X+": "Account Support",
|
"/JE/X+": "Account Support",
|
||||||
"/PCavi": "Public",
|
"/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.",
|
"/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.",
|
||||||
|
"/Xf4UW": "Send anonymous usage metrics",
|
||||||
"/d6vEc": "Make your profile easier to find and share",
|
"/d6vEc": "Make your profile easier to find and share",
|
||||||
"/n5KSF": "{n} ms",
|
"/n5KSF": "{n} ms",
|
||||||
"00LcfG": "Load more",
|
"00LcfG": "Load more",
|
||||||
@ -51,6 +52,7 @@
|
|||||||
"4Z3t5i": "Use imgproxy to compress images",
|
"4Z3t5i": "Use imgproxy to compress images",
|
||||||
"4rYCjn": "Note to Self",
|
"4rYCjn": "Note to Self",
|
||||||
"5BVs2e": "zap",
|
"5BVs2e": "zap",
|
||||||
|
"5CB6zB": "Zap Splits",
|
||||||
"5JcXdV": "Create Account",
|
"5JcXdV": "Create Account",
|
||||||
"5oTnfy": "Buy Handle",
|
"5oTnfy": "Buy Handle",
|
||||||
"5rOdPG": "Once you setup your key manager extension and generated a key, you can follow our new users flow to setup your profile and help you find some interesting people on Nostr to follow.",
|
"5rOdPG": "Once you setup your key manager extension and generated a key, you can follow our new users flow to setup your profile and help you find some interesting people on Nostr to follow.",
|
||||||
@ -59,15 +61,16 @@
|
|||||||
"5ykRmX": "Send zap",
|
"5ykRmX": "Send zap",
|
||||||
"65BmHb": "Failed to proxy image from {host}, click here to load directly",
|
"65BmHb": "Failed to proxy image from {host}, click here to load directly",
|
||||||
"6Yfvvp": "Get an identifier",
|
"6Yfvvp": "Get an identifier",
|
||||||
|
"6bgpn+": "Not all clients support this, you may still receive some zaps as if zap splits was not configured",
|
||||||
"6ewQqw": "Likes ({n})",
|
"6ewQqw": "Likes ({n})",
|
||||||
"6uMqL1": "Unpaid",
|
"6uMqL1": "Unpaid",
|
||||||
"7+Domh": "Notes",
|
"7+Domh": "Notes",
|
||||||
"7BX/yC": "Account Switcher",
|
"7BX/yC": "Account Switcher",
|
||||||
"7hp70g": "NIP-05",
|
"7hp70g": "NIP-05",
|
||||||
"7xzTiH": "{action} to {target}",
|
|
||||||
"8/vBbP": "Reposts ({n})",
|
"8/vBbP": "Reposts ({n})",
|
||||||
"89q5wc": "Confirm Reposts",
|
"89q5wc": "Confirm Reposts",
|
||||||
"8QDesP": "Zap {n} sats",
|
"8QDesP": "Zap {n} sats",
|
||||||
|
"8Y6bZQ": "Invalid zap split: {input}",
|
||||||
"8g2vyB": "name too long",
|
"8g2vyB": "name too long",
|
||||||
"8v1NN+": "Pairing phrase",
|
"8v1NN+": "Pairing phrase",
|
||||||
"9+Ddtu": "Next",
|
"9+Ddtu": "Next",
|
||||||
@ -120,7 +123,6 @@
|
|||||||
"F+B3x1": "We have also partnered with nostrplebs.com to give you more options",
|
"F+B3x1": "We have also partnered with nostrplebs.com to give you more options",
|
||||||
"F3l7xL": "Add Account",
|
"F3l7xL": "Add Account",
|
||||||
"FDguSC": "{n} Zaps",
|
"FDguSC": "{n} Zaps",
|
||||||
"FP+D3H": "LNURL to forward zaps to",
|
|
||||||
"FS3b54": "Done!",
|
"FS3b54": "Done!",
|
||||||
"FSYL8G": "Trending Users",
|
"FSYL8G": "Trending Users",
|
||||||
"FdhSU2": "Claim Now",
|
"FdhSU2": "Claim Now",
|
||||||
@ -143,6 +145,7 @@
|
|||||||
"HOzFdo": "Muted",
|
"HOzFdo": "Muted",
|
||||||
"HWbkEK": "Clear cache and reload",
|
"HWbkEK": "Clear cache and reload",
|
||||||
"HbefNb": "Open Wallet",
|
"HbefNb": "Open Wallet",
|
||||||
|
"I9zn6f": "Pubkey",
|
||||||
"IDjHJ6": "Thanks for using Snort, please consider donating if you can.",
|
"IDjHJ6": "Thanks for using Snort, please consider donating if you can.",
|
||||||
"IEwZvs": "Are you sure you want to unpin this note?",
|
"IEwZvs": "Are you sure you want to unpin this note?",
|
||||||
"IKKHqV": "Follows",
|
"IKKHqV": "Follows",
|
||||||
@ -152,6 +155,7 @@
|
|||||||
"Ix8l+B": "Trending Notes",
|
"Ix8l+B": "Trending Notes",
|
||||||
"J+dIsA": "Subscriptions",
|
"J+dIsA": "Subscriptions",
|
||||||
"JCIgkj": "Username",
|
"JCIgkj": "Username",
|
||||||
|
"JGrt9q": "Send sats to {name}",
|
||||||
"JHEHCk": "Zaps ({n})",
|
"JHEHCk": "Zaps ({n})",
|
||||||
"JPFYIM": "No lightning address",
|
"JPFYIM": "No lightning address",
|
||||||
"JeoS4y": "Repost",
|
"JeoS4y": "Repost",
|
||||||
@ -171,6 +175,7 @@
|
|||||||
"LgbKvU": "Comment",
|
"LgbKvU": "Comment",
|
||||||
"Lu5/Bj": "Open on Zapstr",
|
"Lu5/Bj": "Open on Zapstr",
|
||||||
"Lw+I+J": "{n,plural,=0{{name} zapped} other{{name} & {n} others zapped}}",
|
"Lw+I+J": "{n,plural,=0{{name} zapped} other{{name} & {n} others zapped}}",
|
||||||
|
"LwYmVi": "Zaps on this note will be split to the following users.",
|
||||||
"M3Oirc": "Debug Menus",
|
"M3Oirc": "Debug Menus",
|
||||||
"MBAYRO": "Shows \"Copy ID\" and \"Copy Event JSON\" in the context menu on each message",
|
"MBAYRO": "Shows \"Copy ID\" and \"Copy Event JSON\" in the context menu on each message",
|
||||||
"MI2jkA": "Not available:",
|
"MI2jkA": "Not available:",
|
||||||
@ -193,7 +198,6 @@
|
|||||||
"OLEm6z": "Unknown login error",
|
"OLEm6z": "Unknown login error",
|
||||||
"OQXnew": "You subscription is still active, you can't renew yet",
|
"OQXnew": "You subscription is still active, you can't renew yet",
|
||||||
"ORGv1Q": "Created",
|
"ORGv1Q": "Created",
|
||||||
"P04gQm": "All zaps sent to this note will be received by the following LNURL",
|
|
||||||
"P61BTu": "Copy Event JSON",
|
"P61BTu": "Copy Event JSON",
|
||||||
"P7FD0F": "System (Default)",
|
"P7FD0F": "System (Default)",
|
||||||
"P7nJT9": "Total today (UTC): {amount} sats",
|
"P7nJT9": "Total today (UTC): {amount} sats",
|
||||||
@ -208,7 +212,6 @@
|
|||||||
"QxCuTo": "Art by {name}",
|
"QxCuTo": "Art by {name}",
|
||||||
"Qxv0B2": "You currently have {number} sats in your zap pool.",
|
"Qxv0B2": "You currently have {number} sats in your zap pool.",
|
||||||
"R/6nsx": "Subscription",
|
"R/6nsx": "Subscription",
|
||||||
"R1fEdZ": "Forward Zaps",
|
|
||||||
"R81upa": "People you follow",
|
"R81upa": "People you follow",
|
||||||
"RDZVQL": "Check",
|
"RDZVQL": "Check",
|
||||||
"RahCRH": "Expired",
|
"RahCRH": "Expired",
|
||||||
@ -218,6 +221,7 @@
|
|||||||
"RoOyAh": "Relays",
|
"RoOyAh": "Relays",
|
||||||
"Rs4kCE": "Bookmark",
|
"Rs4kCE": "Bookmark",
|
||||||
"RwFaYs": "Sort",
|
"RwFaYs": "Sort",
|
||||||
|
"SMO+on": "Send zap to {name}",
|
||||||
"SOqbe9": "Update Lightning Address",
|
"SOqbe9": "Update Lightning Address",
|
||||||
"SP0+yi": "Buy Subscription",
|
"SP0+yi": "Buy Subscription",
|
||||||
"SX58hM": "Copy",
|
"SX58hM": "Copy",
|
||||||
@ -247,8 +251,10 @@
|
|||||||
"W2PiAr": "{n} Blocked",
|
"W2PiAr": "{n} Blocked",
|
||||||
"W9355R": "Unmute",
|
"W9355R": "Unmute",
|
||||||
"WONP5O": "Find your twitter follows on nostr (Data provided by {provider})",
|
"WONP5O": "Find your twitter follows on nostr (Data provided by {provider})",
|
||||||
|
"WvGmZT": "npub / nprofile / nostr address",
|
||||||
"WxthCV": "e.g. Jack",
|
"WxthCV": "e.g. Jack",
|
||||||
"X7xU8J": "nsec, npub, nip-05, hex, mnemonic",
|
"X7xU8J": "nsec, npub, nip-05, hex, mnemonic",
|
||||||
|
"XECMfW": "Send usage metrics",
|
||||||
"XICsE8": "File hosts",
|
"XICsE8": "File hosts",
|
||||||
"XgWvGA": "Reactions",
|
"XgWvGA": "Reactions",
|
||||||
"Xopqkl": "Your default zap amount is {number} sats, example values are calculated from this.",
|
"Xopqkl": "Your default zap amount is {number} sats, example values are calculated from this.",
|
||||||
@ -395,6 +401,7 @@
|
|||||||
"rudscU": "Failed to load follows, please try again later",
|
"rudscU": "Failed to load follows, please try again later",
|
||||||
"sUNhQE": "user",
|
"sUNhQE": "user",
|
||||||
"sWnYKw": "Snort is designed to have a similar experience to Twitter.",
|
"sWnYKw": "Snort is designed to have a similar experience to Twitter.",
|
||||||
|
"sZQzjQ": "Failed to parse zap split: {input}",
|
||||||
"svOoEH": "Name-squatting and impersonation is not allowed. Snort and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.",
|
"svOoEH": "Name-squatting and impersonation is not allowed. Snort and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.",
|
||||||
"tOdNiY": "Dark",
|
"tOdNiY": "Dark",
|
||||||
"th5lxp": "Send note to a subset of your write relays",
|
"th5lxp": "Send note to a subset of your write relays",
|
||||||
@ -402,7 +409,6 @@
|
|||||||
"ttxS0b": "Supporter Badge",
|
"ttxS0b": "Supporter Badge",
|
||||||
"u/vOPu": "Paid",
|
"u/vOPu": "Paid",
|
||||||
"u4bHcR": "Check out the code here: {link}",
|
"u4bHcR": "Check out the code here: {link}",
|
||||||
"uD/N6c": "Zap {target} {n} sats",
|
|
||||||
"uSV4Ti": "Reposts need to be manually confirmed",
|
"uSV4Ti": "Reposts need to be manually confirmed",
|
||||||
"usAvMr": "Edit Profile",
|
"usAvMr": "Edit Profile",
|
||||||
"ut+2Cd": "Get a partner identifier",
|
"ut+2Cd": "Get a partner identifier",
|
||||||
@ -436,6 +442,7 @@
|
|||||||
"y1Z3or": "Language",
|
"y1Z3or": "Language",
|
||||||
"yCLnBC": "LNURL or Lightning Address",
|
"yCLnBC": "LNURL or Lightning Address",
|
||||||
"yCmnnm": "Read global from",
|
"yCmnnm": "Read global from",
|
||||||
|
"zCb8fX": "Weight",
|
||||||
"zFegDD": "Contact",
|
"zFegDD": "Contact",
|
||||||
"zINlao": "Owner",
|
"zINlao": "Owner",
|
||||||
"zQvVDJ": "All",
|
"zQvVDJ": "All",
|
||||||
|
@ -92,17 +92,6 @@ export class LNURL {
|
|||||||
return `${username}@${this.#url.hostname}`;
|
return `${username}@${this.#url.hostname}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a NIP-57 zap tag from this LNURL
|
|
||||||
*/
|
|
||||||
getZapTag() {
|
|
||||||
if (this.isLNAddress) {
|
|
||||||
return ["zap", this.getLNAddress(), "lud16"];
|
|
||||||
} else {
|
|
||||||
return ["zap", this.#url.toString(), "lud06"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const rsp = await fetch(this.#url);
|
const rsp = await fetch(this.#url);
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
|
@ -43,6 +43,10 @@ export function unixNowMs() {
|
|||||||
return new Date().getTime();
|
return new Date().getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function jitter(n: number) {
|
||||||
|
return n * 2 * Math.random() - n;
|
||||||
|
}
|
||||||
|
|
||||||
export function deepClone<T>(obj: T) {
|
export function deepClone<T>(obj: T) {
|
||||||
if ("structuredClone" in window) {
|
if ("structuredClone" in window) {
|
||||||
return structuredClone(obj);
|
return structuredClone(obj);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { EventKind, HexKey, NostrPrefix, NostrEvent, EventSigner, PowMiner } from ".";
|
import { EventKind, HexKey, NostrPrefix, NostrEvent, EventSigner, PowMiner } from ".";
|
||||||
import { HashtagRegex, MentionNostrEntityRegex } from "./const";
|
import { HashtagRegex, MentionNostrEntityRegex } from "./const";
|
||||||
import { getPublicKey, unixNow } from "@snort/shared";
|
import { getPublicKey, jitter, unixNow } from "@snort/shared";
|
||||||
import { EventExt } from "./event-ext";
|
import { EventExt } from "./event-ext";
|
||||||
import { tryParseNostrLink } from "./nostr-link";
|
import { tryParseNostrLink } from "./nostr-link";
|
||||||
|
|
||||||
@ -12,6 +12,12 @@ export class EventBuilder {
|
|||||||
#tags: Array<Array<string>> = [];
|
#tags: Array<Array<string>> = [];
|
||||||
#pow?: number;
|
#pow?: number;
|
||||||
#powMiner?: PowMiner;
|
#powMiner?: PowMiner;
|
||||||
|
#jitter?: number;
|
||||||
|
|
||||||
|
jitter(n: number) {
|
||||||
|
this.#jitter = n;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
kind(k: EventKind) {
|
kind(k: EventKind) {
|
||||||
this.#kind = k;
|
this.#kind = k;
|
||||||
@ -73,8 +79,8 @@ export class EventBuilder {
|
|||||||
pubkey: this.#pubkey ?? "",
|
pubkey: this.#pubkey ?? "",
|
||||||
content: this.#content ?? "",
|
content: this.#content ?? "",
|
||||||
kind: this.#kind,
|
kind: this.#kind,
|
||||||
created_at: this.#createdAt ?? unixNow(),
|
created_at: (this.#createdAt ?? unixNow()) + (this.#jitter ? jitter(this.#jitter) : 0),
|
||||||
tags: this.#tags,
|
tags: this.#tags.sort((a, b) => a[0].localeCompare(b[0])),
|
||||||
} as NostrEvent;
|
} as NostrEvent;
|
||||||
ev.id = EventExt.createId(ev);
|
ev.id = EventExt.createId(ev);
|
||||||
return ev;
|
return ev;
|
||||||
|
@ -11,6 +11,17 @@ export interface NostrLink {
|
|||||||
encode(): string;
|
encode(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function linkToEventTag(link: NostrLink) {
|
||||||
|
const relayEntry = link.relays ? [link.relays[0]] : [];
|
||||||
|
if (link.type === NostrPrefix.PublicKey) {
|
||||||
|
return ["p", link.id];
|
||||||
|
} else if (link.type === NostrPrefix.Note || link.type === NostrPrefix.Event) {
|
||||||
|
return ["e", link.id];
|
||||||
|
} else if (link.type === NostrPrefix.Address) {
|
||||||
|
return ["a", `${link.kind}:${link.author}:${link.id}`, ...relayEntry];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createNostrLinkToEvent(ev: TaggedNostrEvent | NostrEvent) {
|
export function createNostrLinkToEvent(ev: TaggedNostrEvent | NostrEvent) {
|
||||||
const relays = "relays" in ev ? ev.relays : undefined;
|
const relays = "relays" in ev ? ev.relays : undefined;
|
||||||
|
|
||||||
|
@ -64,6 +64,25 @@ export class ProfileLoaderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchProfile(key: string) {
|
||||||
|
const existing = this.Cache.get(key);
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
} else {
|
||||||
|
return await new Promise<MetadataCache>((resolve, reject) => {
|
||||||
|
this.TrackMetadata(key);
|
||||||
|
const release = this.Cache.hook(() => {
|
||||||
|
const existing = this.Cache.getFromCache(key);
|
||||||
|
if (existing) {
|
||||||
|
resolve(existing);
|
||||||
|
release();
|
||||||
|
this.UntrackMetadata(key);
|
||||||
|
}
|
||||||
|
}, key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async #FetchMetadata() {
|
async #FetchMetadata() {
|
||||||
const missingFromCache = await this.#cache.buffer([...this.#wantsMetadata]);
|
const missingFromCache = await this.#cache.buffer([...this.#wantsMetadata]);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user