Merge pull request #466 from v0l/per-event-zap-targets
Per event zap targets
This commit is contained in:
commit
5944cfd918
@ -9,6 +9,7 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 42;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import "./NoteCreator.css";
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
|
||||
|
||||
import Icon from "Icons/Icon";
|
||||
@ -11,6 +11,7 @@ import Modal from "Element/Modal";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import useFileUpload from "Upload";
|
||||
import Note from "Element/Note";
|
||||
import { LNURL } from "LNURL";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -40,16 +41,34 @@ export interface NoteCreatorProps {
|
||||
|
||||
export function NoteCreator(props: NoteCreatorProps) {
|
||||
const { show, setShow, replyTo, onSend, autoFocus } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const [note, setNote] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [active, setActive] = useState(false);
|
||||
const [preview, setPreview] = useState<RawEvent>();
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [zapForward, setZapForward] = useState("");
|
||||
const uploader = useFileUpload();
|
||||
|
||||
async function sendNote() {
|
||||
if (note) {
|
||||
const ev = replyTo ? await publisher.reply(replyTo, note) : await publisher.note(note);
|
||||
let extraTags: Array<Array<string>> | undefined;
|
||||
if (zapForward) {
|
||||
try {
|
||||
const svc = new LNURL(zapForward);
|
||||
await svc.load();
|
||||
extraTags = [svc.getZapTag()];
|
||||
} catch {
|
||||
setError(
|
||||
formatMessage({
|
||||
defaultMessage: "Invalid LNURL",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const ev = replyTo ? await publisher.reply(replyTo, note, extraTags) : await publisher.note(note, extraTags);
|
||||
console.debug("Sending note: ", ev);
|
||||
publisher.broadcast(ev);
|
||||
setNote("");
|
||||
@ -92,6 +111,9 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
function cancel() {
|
||||
setShow(false);
|
||||
setNote("");
|
||||
setShowAdvanced(false);
|
||||
setPreview(undefined);
|
||||
setZapForward("");
|
||||
}
|
||||
|
||||
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
|
||||
@ -149,8 +171,8 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
</div>
|
||||
)}
|
||||
<div className="note-creator-actions">
|
||||
<button className="secondary" type="button" onClick={loadPreview}>
|
||||
<FormattedMessage defaultMessage="Toggle Preview" />
|
||||
<button className="secondary" type="button" onClick={() => setShowAdvanced(s => !s)}>
|
||||
<FormattedMessage defaultMessage="Advanced" />
|
||||
</button>
|
||||
<button className="secondary" type="button" onClick={cancel}>
|
||||
<FormattedMessage {...messages.Cancel} />
|
||||
@ -159,6 +181,28 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
{replyTo ? <FormattedMessage {...messages.Reply} /> : <FormattedMessage {...messages.Send} />}
|
||||
</button>
|
||||
</div>
|
||||
{showAdvanced && (
|
||||
<div>
|
||||
<button className="secondary" type="button" onClick={loadPreview}>
|
||||
<FormattedMessage defaultMessage="Toggle Preview" />
|
||||
</button>
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Forward Zaps" />
|
||||
</h4>
|
||||
<p>
|
||||
<FormattedMessage defaultMessage="All zaps sent to this note will be received by the following LNURL" />
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
className="w-max"
|
||||
placeholder={formatMessage({
|
||||
defaultMessage: "LNURL to forward zaps to",
|
||||
})}
|
||||
value={zapForward}
|
||||
onChange={e => setZapForward(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
|
@ -153,10 +153,23 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function getLNURL() {
|
||||
return ev.tags.find(a => a[0] === "zap")?.[1] || author?.lud16 || author?.lud06;
|
||||
}
|
||||
|
||||
function getTargetName() {
|
||||
const zapTarget = ev.tags.find(a => a[0] === "zap")?.[1];
|
||||
if (zapTarget) {
|
||||
return new LNURL(zapTarget).name;
|
||||
} else {
|
||||
return author?.display_name || author?.name;
|
||||
}
|
||||
}
|
||||
|
||||
async function fastZap(e?: React.MouseEvent) {
|
||||
if (zapping || e?.isPropagationStopped()) return;
|
||||
|
||||
const lnurl = author?.lud16 || author?.lud06;
|
||||
const lnurl = getLNURL();
|
||||
if (wallet?.isReady() && lnurl) {
|
||||
setZapping(true);
|
||||
try {
|
||||
@ -203,7 +216,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (prefs.autoZap && !ZapCache.has(ev.id) && !isMine && !zapping) {
|
||||
const lnurl = author?.lud16 || author?.lud06;
|
||||
const lnurl = getLNURL();
|
||||
if (wallet?.isReady() && lnurl) {
|
||||
setZapping(true);
|
||||
queueMicrotask(async () => {
|
||||
@ -222,7 +235,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
}, [prefs.autoZap, author, zapping]);
|
||||
|
||||
function tipButton() {
|
||||
const service = author?.lud16 || author?.lud06;
|
||||
const service = getLNURL();
|
||||
if (service) {
|
||||
return (
|
||||
<>
|
||||
@ -418,11 +431,11 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
zaps={zaps}
|
||||
/>
|
||||
<SendSats
|
||||
lnurl={author?.lud16 || author?.lud06}
|
||||
lnurl={getLNURL()}
|
||||
onClose={() => setTip(false)}
|
||||
show={tip}
|
||||
author={author?.pubkey}
|
||||
target={author?.display_name || author?.name}
|
||||
target={getTargetName()}
|
||||
note={ev.id}
|
||||
/>
|
||||
</div>
|
||||
|
@ -176,10 +176,15 @@ export default function useEventPublisher() {
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
note: async (msg: string) => {
|
||||
note: async (msg: string, extraTags?: Array<Array<string>>) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.TextNote);
|
||||
processContent(ev, msg);
|
||||
if (extraTags) {
|
||||
for (const et of extraTags) {
|
||||
ev.tags.push(et);
|
||||
}
|
||||
}
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
@ -200,7 +205,7 @@ export default function useEventPublisher() {
|
||||
/**
|
||||
* Reply to a note
|
||||
*/
|
||||
reply: async (replyTo: TaggedRawEvent, msg: string) => {
|
||||
reply: async (replyTo: TaggedRawEvent, msg: string, extraTags?: Array<Array<string>>) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.TextNote);
|
||||
|
||||
@ -230,6 +235,11 @@ export default function useEventPublisher() {
|
||||
}
|
||||
}
|
||||
processContent(ev, msg);
|
||||
if (extraTags) {
|
||||
for (const et of extraTags) {
|
||||
ev.tags.push(et);
|
||||
}
|
||||
}
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
|
@ -48,6 +48,62 @@ export class LNURL {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URL of this payService
|
||||
*/
|
||||
get url() {
|
||||
return this.#url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the optimal formatted LNURL
|
||||
*/
|
||||
get lnurl() {
|
||||
if (this.isLNAddress) {
|
||||
return this.getLNAddress();
|
||||
}
|
||||
return this.#url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Human readable name for this service
|
||||
*/
|
||||
get name() {
|
||||
// LN Address formatted URL
|
||||
if (this.isLNAddress) {
|
||||
return this.getLNAddress();
|
||||
}
|
||||
// Generic LUD-06 url
|
||||
return this.#url.hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this LNURL a LUD-16 Lightning Address
|
||||
*/
|
||||
get isLNAddress() {
|
||||
return this.#url.pathname.startsWith("/.well-known/lnurlp/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the LN Address for this LNURL
|
||||
*/
|
||||
getLNAddress() {
|
||||
const pathParts = this.#url.pathname.split("/");
|
||||
const username = pathParts[pathParts.length - 1];
|
||||
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() {
|
||||
const rsp = await fetch(this.#url);
|
||||
if (rsp.ok) {
|
||||
|
@ -24,7 +24,7 @@ import useModeration from "Hooks/useModeration";
|
||||
import useZapsFeed from "Feed/ZapsFeed";
|
||||
import { default as ZapElement } from "Element/Zap";
|
||||
import FollowButton from "Element/FollowButton";
|
||||
import { extractLnAddress, parseId, hexToBech32 } from "Util";
|
||||
import { parseId, hexToBech32 } from "Util";
|
||||
import Avatar from "Element/Avatar";
|
||||
import Timeline from "Element/Timeline";
|
||||
import Text from "Element/Text";
|
||||
@ -43,9 +43,11 @@ import Modal from "Element/Modal";
|
||||
import BadgeList from "Element/BadgeList";
|
||||
import { ProxyImg } from "Element/ProxyImg";
|
||||
import useHorizontalScroll from "Hooks/useHorizontalScroll";
|
||||
import messages from "./messages";
|
||||
import { EmailRegex } from "Const";
|
||||
import { getNip05PubKey } from "./Login";
|
||||
import { getNip05PubKey } from "Pages/Login";
|
||||
import { LNURL } from "LNURL";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
const NOTES = 0;
|
||||
const REACTIONS = 1;
|
||||
@ -111,7 +113,13 @@ export default function ProfilePage() {
|
||||
});
|
||||
const npub = !id?.startsWith(NostrPrefix.PublicKey) ? hexToBech32(NostrPrefix.PublicKey, id || undefined) : id;
|
||||
|
||||
const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || "");
|
||||
const lnurl = (() => {
|
||||
try {
|
||||
return new LNURL(user?.lud16 || user?.lud06 || "");
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
})();
|
||||
const website_url =
|
||||
user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || "";
|
||||
// feeds
|
||||
@ -185,12 +193,12 @@ export default function ProfilePage() {
|
||||
{lnurl && (
|
||||
<div className="lnurl f-ellipsis" onClick={() => setShowLnQr(true)}>
|
||||
<Icon name="zap" />
|
||||
{lnurl}
|
||||
{lnurl.name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SendSats
|
||||
lnurl={lnurl}
|
||||
lnurl={lnurl?.lnurl}
|
||||
show={showLnQr}
|
||||
onClose={() => setShowLnQr(false)}
|
||||
author={id}
|
||||
|
@ -146,26 +146,6 @@ export function getAllReactions(notes: readonly TaggedRawEvent[] | undefined, id
|
||||
return notes?.filter(a => a.kind === (kind ?? a.kind) && a.tags.some(a => a[0] === "e" && ids.includes(a[1]))) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts LNURL service to LN Address
|
||||
*/
|
||||
export function extractLnAddress(lnurl: string) {
|
||||
// some clients incorrectly set this to LNURL service, patch this
|
||||
if (lnurl.toLowerCase().startsWith("lnurl")) {
|
||||
const url = bech32ToText(lnurl);
|
||||
if (url.startsWith("http")) {
|
||||
const parsedUri = new URL(url);
|
||||
// is lightning address
|
||||
if (parsedUri.pathname.startsWith("/.well-known/lnurlp/")) {
|
||||
const pathParts = parsedUri.pathname.split("/");
|
||||
const username = pathParts[pathParts.length - 1];
|
||||
return `${username}@${parsedUri.hostname}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return lnurl;
|
||||
}
|
||||
|
||||
export function unixNow() {
|
||||
return Math.floor(unixNowMs() / 1000);
|
||||
}
|
||||
|
@ -78,6 +78,9 @@
|
||||
"2k0Cv+": {
|
||||
"defaultMessage": "Dislikes ({n})"
|
||||
},
|
||||
"3Rx6Qo": {
|
||||
"defaultMessage": "Advanced"
|
||||
},
|
||||
"3cc4Ct": {
|
||||
"defaultMessage": "Light"
|
||||
},
|
||||
@ -278,6 +281,9 @@
|
||||
"FDguSC": {
|
||||
"defaultMessage": "{n} Zaps"
|
||||
},
|
||||
"FP+D3H": {
|
||||
"defaultMessage": "LNURL to forward zaps to"
|
||||
},
|
||||
"FS3b54": {
|
||||
"defaultMessage": "Done!"
|
||||
},
|
||||
@ -437,6 +443,9 @@
|
||||
"OLEm6z": {
|
||||
"defaultMessage": "Unknown login error"
|
||||
},
|
||||
"P04gQm": {
|
||||
"defaultMessage": "All zaps sent to this note will be received by the following LNURL"
|
||||
},
|
||||
"P61BTu": {
|
||||
"defaultMessage": "Copy Event JSON"
|
||||
},
|
||||
@ -472,6 +481,9 @@
|
||||
"defaultMessage": "Art by {name}",
|
||||
"description": "Artwork attribution label"
|
||||
},
|
||||
"R1fEdZ": {
|
||||
"defaultMessage": "Forward Zaps"
|
||||
},
|
||||
"R2OqnW": {
|
||||
"defaultMessage": "Delete Account"
|
||||
},
|
||||
|
@ -25,6 +25,7 @@
|
||||
"2LbrkB": "Enter password",
|
||||
"2a2YiP": "{n} Bookmarks",
|
||||
"2k0Cv+": "Dislikes ({n})",
|
||||
"3Rx6Qo": "Advanced",
|
||||
"3cc4Ct": "Light",
|
||||
"3gOsZq": "Translators",
|
||||
"3t3kok": "{n,plural,=1{{n} new note} other{{n} new notes}}",
|
||||
@ -90,6 +91,7 @@
|
||||
"Eqjl5K": "Only Snort and our integration partner identifier gives you a colorful domain name, but you are welcome to use other services too.",
|
||||
"F+B3x1": "We have also partnered with nostrplebs.com to give you more options",
|
||||
"FDguSC": "{n} Zaps",
|
||||
"FP+D3H": "LNURL to forward zaps to",
|
||||
"FS3b54": "Done!",
|
||||
"FfYsOb": "An error has occured!",
|
||||
"FmXUJg": "follows you",
|
||||
@ -142,6 +144,7 @@
|
||||
"OEW7yJ": "Zaps",
|
||||
"OKhRC6": "Share",
|
||||
"OLEm6z": "Unknown login error",
|
||||
"P04gQm": "All zaps sent to this note will be received by the following LNURL",
|
||||
"P61BTu": "Copy Event JSON",
|
||||
"P7FD0F": "System (Default)",
|
||||
"P7nJT9": "Total today (UTC): {amount} sats",
|
||||
@ -153,6 +156,7 @@
|
||||
"QTdJfH": "Create an Account",
|
||||
"QawghE": "You can change your username at any point.",
|
||||
"QxCuTo": "Art by {name}",
|
||||
"R1fEdZ": "Forward Zaps",
|
||||
"R2OqnW": "Delete Account",
|
||||
"RDZVQL": "Check",
|
||||
"RahCRH": "Expired",
|
||||
|
@ -12,6 +12,7 @@ export default class Tag {
|
||||
DTag?: string;
|
||||
Index: number;
|
||||
Invalid: boolean;
|
||||
LNURL?: string;
|
||||
|
||||
constructor(tag: string[], index: number) {
|
||||
this.Original = tag;
|
||||
@ -50,6 +51,10 @@ export default class Tag {
|
||||
this.PubKey = tag[1];
|
||||
break;
|
||||
}
|
||||
case "zap": {
|
||||
this.LNURL = tag[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user