Merge pull request #466 from v0l/per-event-zap-targets

Per event zap targets
This commit is contained in:
Kieran 2023-04-05 12:14:57 +01:00 committed by GitHub
commit 5944cfd918
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 170 additions and 37 deletions

View File

@ -9,6 +9,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 42; z-index: 42;
overflow-y: auto;
} }
.modal-body { .modal-body {

View File

@ -1,6 +1,6 @@
import "./NoteCreator.css"; import "./NoteCreator.css";
import { useState } from "react"; import { useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { RawEvent, TaggedRawEvent } from "@snort/nostr"; import { RawEvent, TaggedRawEvent } from "@snort/nostr";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
@ -11,6 +11,7 @@ import Modal from "Element/Modal";
import ProfileImage from "Element/ProfileImage"; import ProfileImage from "Element/ProfileImage";
import useFileUpload from "Upload"; import useFileUpload from "Upload";
import Note from "Element/Note"; import Note from "Element/Note";
import { LNURL } from "LNURL";
import messages from "./messages"; import messages from "./messages";
@ -40,16 +41,34 @@ export interface NoteCreatorProps {
export function NoteCreator(props: NoteCreatorProps) { export function NoteCreator(props: NoteCreatorProps) {
const { show, setShow, replyTo, onSend, autoFocus } = props; const { show, setShow, replyTo, onSend, autoFocus } = props;
const { formatMessage } = useIntl();
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const [note, setNote] = useState(""); const [note, setNote] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const [preview, setPreview] = useState<RawEvent>(); const [preview, setPreview] = useState<RawEvent>();
const [showAdvanced, setShowAdvanced] = useState(false);
const [zapForward, setZapForward] = useState("");
const uploader = useFileUpload(); const uploader = useFileUpload();
async function sendNote() { async function sendNote() {
if (note) { 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); console.debug("Sending note: ", ev);
publisher.broadcast(ev); publisher.broadcast(ev);
setNote(""); setNote("");
@ -92,6 +111,9 @@ export function NoteCreator(props: NoteCreatorProps) {
function cancel() { function cancel() {
setShow(false); setShow(false);
setNote(""); setNote("");
setShowAdvanced(false);
setPreview(undefined);
setZapForward("");
} }
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) { function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
@ -149,8 +171,8 @@ export function NoteCreator(props: NoteCreatorProps) {
</div> </div>
)} )}
<div className="note-creator-actions"> <div className="note-creator-actions">
<button className="secondary" type="button" onClick={loadPreview}> <button className="secondary" type="button" onClick={() => setShowAdvanced(s => !s)}>
<FormattedMessage defaultMessage="Toggle Preview" /> <FormattedMessage defaultMessage="Advanced" />
</button> </button>
<button className="secondary" type="button" onClick={cancel}> <button className="secondary" type="button" onClick={cancel}>
<FormattedMessage {...messages.Cancel} /> <FormattedMessage {...messages.Cancel} />
@ -159,6 +181,28 @@ export function NoteCreator(props: NoteCreatorProps) {
{replyTo ? <FormattedMessage {...messages.Reply} /> : <FormattedMessage {...messages.Send} />} {replyTo ? <FormattedMessage {...messages.Reply} /> : <FormattedMessage {...messages.Send} />}
</button> </button>
</div> </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> </Modal>
)} )}
</> </>

View File

@ -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) { async function fastZap(e?: React.MouseEvent) {
if (zapping || e?.isPropagationStopped()) return; if (zapping || e?.isPropagationStopped()) return;
const lnurl = author?.lud16 || author?.lud06; const lnurl = getLNURL();
if (wallet?.isReady() && lnurl) { if (wallet?.isReady() && lnurl) {
setZapping(true); setZapping(true);
try { try {
@ -203,7 +216,7 @@ export default function NoteFooter(props: NoteFooterProps) {
useEffect(() => { useEffect(() => {
if (prefs.autoZap && !ZapCache.has(ev.id) && !isMine && !zapping) { if (prefs.autoZap && !ZapCache.has(ev.id) && !isMine && !zapping) {
const lnurl = author?.lud16 || author?.lud06; const lnurl = getLNURL();
if (wallet?.isReady() && lnurl) { if (wallet?.isReady() && lnurl) {
setZapping(true); setZapping(true);
queueMicrotask(async () => { queueMicrotask(async () => {
@ -222,7 +235,7 @@ export default function NoteFooter(props: NoteFooterProps) {
}, [prefs.autoZap, author, zapping]); }, [prefs.autoZap, author, zapping]);
function tipButton() { function tipButton() {
const service = author?.lud16 || author?.lud06; const service = getLNURL();
if (service) { if (service) {
return ( return (
<> <>
@ -418,11 +431,11 @@ export default function NoteFooter(props: NoteFooterProps) {
zaps={zaps} zaps={zaps}
/> />
<SendSats <SendSats
lnurl={author?.lud16 || author?.lud06} lnurl={getLNURL()}
onClose={() => setTip(false)} onClose={() => setTip(false)}
show={tip} show={tip}
author={author?.pubkey} author={author?.pubkey}
target={author?.display_name || author?.name} target={getTargetName()}
note={ev.id} note={ev.id}
/> />
</div> </div>

View File

@ -176,10 +176,15 @@ export default function useEventPublisher() {
return await signEvent(ev); return await signEvent(ev);
} }
}, },
note: async (msg: string) => { note: async (msg: string, extraTags?: Array<Array<string>>) => {
if (pubKey) { if (pubKey) {
const ev = EventExt.forPubKey(pubKey, EventKind.TextNote); const ev = EventExt.forPubKey(pubKey, EventKind.TextNote);
processContent(ev, msg); processContent(ev, msg);
if (extraTags) {
for (const et of extraTags) {
ev.tags.push(et);
}
}
return await signEvent(ev); return await signEvent(ev);
} }
}, },
@ -200,7 +205,7 @@ export default function useEventPublisher() {
/** /**
* Reply to a note * Reply to a note
*/ */
reply: async (replyTo: TaggedRawEvent, msg: string) => { reply: async (replyTo: TaggedRawEvent, msg: string, extraTags?: Array<Array<string>>) => {
if (pubKey) { if (pubKey) {
const ev = EventExt.forPubKey(pubKey, EventKind.TextNote); const ev = EventExt.forPubKey(pubKey, EventKind.TextNote);
@ -230,6 +235,11 @@ export default function useEventPublisher() {
} }
} }
processContent(ev, msg); processContent(ev, msg);
if (extraTags) {
for (const et of extraTags) {
ev.tags.push(et);
}
}
return await signEvent(ev); return await signEvent(ev);
} }
}, },

View File

@ -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() { async load() {
const rsp = await fetch(this.#url); const rsp = await fetch(this.#url);
if (rsp.ok) { if (rsp.ok) {

View File

@ -24,7 +24,7 @@ import useModeration from "Hooks/useModeration";
import useZapsFeed from "Feed/ZapsFeed"; import useZapsFeed from "Feed/ZapsFeed";
import { default as ZapElement } from "Element/Zap"; import { default as ZapElement } from "Element/Zap";
import FollowButton from "Element/FollowButton"; import FollowButton from "Element/FollowButton";
import { extractLnAddress, parseId, hexToBech32 } from "Util"; import { parseId, hexToBech32 } from "Util";
import Avatar from "Element/Avatar"; import Avatar from "Element/Avatar";
import Timeline from "Element/Timeline"; import Timeline from "Element/Timeline";
import Text from "Element/Text"; import Text from "Element/Text";
@ -43,9 +43,11 @@ import Modal from "Element/Modal";
import BadgeList from "Element/BadgeList"; import BadgeList from "Element/BadgeList";
import { ProxyImg } from "Element/ProxyImg"; import { ProxyImg } from "Element/ProxyImg";
import useHorizontalScroll from "Hooks/useHorizontalScroll"; import useHorizontalScroll from "Hooks/useHorizontalScroll";
import messages from "./messages";
import { EmailRegex } from "Const"; 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 NOTES = 0;
const REACTIONS = 1; const REACTIONS = 1;
@ -111,7 +113,13 @@ export default function ProfilePage() {
}); });
const npub = !id?.startsWith(NostrPrefix.PublicKey) ? hexToBech32(NostrPrefix.PublicKey, id || undefined) : id; 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 = const website_url =
user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || ""; user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || "";
// feeds // feeds
@ -185,12 +193,12 @@ export default function ProfilePage() {
{lnurl && ( {lnurl && (
<div className="lnurl f-ellipsis" onClick={() => setShowLnQr(true)}> <div className="lnurl f-ellipsis" onClick={() => setShowLnQr(true)}>
<Icon name="zap" /> <Icon name="zap" />
{lnurl} {lnurl.name}
</div> </div>
)} )}
<SendSats <SendSats
lnurl={lnurl} lnurl={lnurl?.lnurl}
show={showLnQr} show={showLnQr}
onClose={() => setShowLnQr(false)} onClose={() => setShowLnQr(false)}
author={id} author={id}

View File

@ -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]))) || []; 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() { export function unixNow() {
return Math.floor(unixNowMs() / 1000); return Math.floor(unixNowMs() / 1000);
} }

View File

@ -78,6 +78,9 @@
"2k0Cv+": { "2k0Cv+": {
"defaultMessage": "Dislikes ({n})" "defaultMessage": "Dislikes ({n})"
}, },
"3Rx6Qo": {
"defaultMessage": "Advanced"
},
"3cc4Ct": { "3cc4Ct": {
"defaultMessage": "Light" "defaultMessage": "Light"
}, },
@ -278,6 +281,9 @@
"FDguSC": { "FDguSC": {
"defaultMessage": "{n} Zaps" "defaultMessage": "{n} Zaps"
}, },
"FP+D3H": {
"defaultMessage": "LNURL to forward zaps to"
},
"FS3b54": { "FS3b54": {
"defaultMessage": "Done!" "defaultMessage": "Done!"
}, },
@ -437,6 +443,9 @@
"OLEm6z": { "OLEm6z": {
"defaultMessage": "Unknown login error" "defaultMessage": "Unknown login error"
}, },
"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"
}, },
@ -472,6 +481,9 @@
"defaultMessage": "Art by {name}", "defaultMessage": "Art by {name}",
"description": "Artwork attribution label" "description": "Artwork attribution label"
}, },
"R1fEdZ": {
"defaultMessage": "Forward Zaps"
},
"R2OqnW": { "R2OqnW": {
"defaultMessage": "Delete Account" "defaultMessage": "Delete Account"
}, },

View File

@ -25,6 +25,7 @@
"2LbrkB": "Enter password", "2LbrkB": "Enter password",
"2a2YiP": "{n} Bookmarks", "2a2YiP": "{n} Bookmarks",
"2k0Cv+": "Dislikes ({n})", "2k0Cv+": "Dislikes ({n})",
"3Rx6Qo": "Advanced",
"3cc4Ct": "Light", "3cc4Ct": "Light",
"3gOsZq": "Translators", "3gOsZq": "Translators",
"3t3kok": "{n,plural,=1{{n} new note} other{{n} new notes}}", "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.", "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", "F+B3x1": "We have also partnered with nostrplebs.com to give you more options",
"FDguSC": "{n} Zaps", "FDguSC": "{n} Zaps",
"FP+D3H": "LNURL to forward zaps to",
"FS3b54": "Done!", "FS3b54": "Done!",
"FfYsOb": "An error has occured!", "FfYsOb": "An error has occured!",
"FmXUJg": "follows you", "FmXUJg": "follows you",
@ -142,6 +144,7 @@
"OEW7yJ": "Zaps", "OEW7yJ": "Zaps",
"OKhRC6": "Share", "OKhRC6": "Share",
"OLEm6z": "Unknown login error", "OLEm6z": "Unknown login error",
"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",
@ -153,6 +156,7 @@
"QTdJfH": "Create an Account", "QTdJfH": "Create an Account",
"QawghE": "You can change your username at any point.", "QawghE": "You can change your username at any point.",
"QxCuTo": "Art by {name}", "QxCuTo": "Art by {name}",
"R1fEdZ": "Forward Zaps",
"R2OqnW": "Delete Account", "R2OqnW": "Delete Account",
"RDZVQL": "Check", "RDZVQL": "Check",
"RahCRH": "Expired", "RahCRH": "Expired",

View File

@ -12,6 +12,7 @@ export default class Tag {
DTag?: string; DTag?: string;
Index: number; Index: number;
Invalid: boolean; Invalid: boolean;
LNURL?: string;
constructor(tag: string[], index: number) { constructor(tag: string[], index: number) {
this.Original = tag; this.Original = tag;
@ -50,6 +51,10 @@ export default class Tag {
this.PubKey = tag[1]; this.PubKey = tag[1];
break; break;
} }
case "zap": {
this.LNURL = tag[1];
break;
}
} }
} }