From 8ef20c27b33ba0097fe763e22441a978203ab650 Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 27 Mar 2023 23:58:29 +0100 Subject: [PATCH 1/4] feat: per event zap targets --- packages/app/src/Element/NoteCreator.tsx | 45 ++++++++++++++++++++++-- packages/app/src/Element/NoteFooter.tsx | 18 +++++++--- packages/app/src/Feed/EventPublisher.ts | 14 ++++++-- packages/app/src/lang.json | 12 +++++++ packages/app/src/translations/en.json | 6 +++- packages/nostr/src/legacy/Tag.ts | 5 +++ 6 files changed, 90 insertions(+), 10 deletions(-) diff --git a/packages/app/src/Element/NoteCreator.tsx b/packages/app/src/Element/NoteCreator.tsx index cada2502..825a7f94 100644 --- a/packages/app/src/Element/NoteCreator.tsx +++ b/packages/app/src/Element/NoteCreator.tsx @@ -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(); + 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> | undefined; + if (zapForward) { + try { + const svc = new LNURL(zapForward); + await svc.load(); + } catch { + setError( + formatMessage({ + defaultMessage: "Invalid LNURL", + }) + ); + return; + } + extraTags = [["zap", zapForward]]; + } + const ev = replyTo ? await publisher.reply(replyTo, note, extraTags) : await publisher.note(note, extraTags); console.debug("Sending note: ", ev); publisher.broadcast(ev); setNote(""); @@ -152,6 +171,9 @@ export function NoteCreator(props: NoteCreatorProps) { + @@ -159,6 +181,25 @@ export function NoteCreator(props: NoteCreatorProps) { {replyTo ? : } + {showAdvanced && ( + <> +

+ +

+

+ +

+ setZapForward(e.target.value)} + /> + + )} )} diff --git a/packages/app/src/Element/NoteFooter.tsx b/packages/app/src/Element/NoteFooter.tsx index dafba60c..f11e7cdb 100644 --- a/packages/app/src/Element/NoteFooter.tsx +++ b/packages/app/src/Element/NoteFooter.tsx @@ -153,10 +153,18 @@ export default function NoteFooter(props: NoteFooterProps) { } } + function getLNURL() { + return ev.Tags.find(a => a.Key === "zap")?.LNURL || author?.lud16 || author?.lud06; + } + + function getTargetName() { + return ev.Tags.find(a => a.Key === "zap")?.LNURL || 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 +211,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 +230,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 +426,11 @@ export default function NoteFooter(props: NoteFooterProps) { zaps={zaps} /> setTip(false)} show={tip} author={author?.pubkey} - target={author?.display_name || author?.name} + target={getTargetName()} note={ev.id} /> diff --git a/packages/app/src/Feed/EventPublisher.ts b/packages/app/src/Feed/EventPublisher.ts index e4769b65..f83a9d1e 100644 --- a/packages/app/src/Feed/EventPublisher.ts +++ b/packages/app/src/Feed/EventPublisher.ts @@ -176,10 +176,15 @@ export default function useEventPublisher() { return await signEvent(ev); } }, - note: async (msg: string) => { + note: async (msg: string, extraTags?: Array>) => { 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>) => { 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); } }, diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index a36641fe..806dd221 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -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" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 592d6406..8bb7e76b 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -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", @@ -321,4 +325,4 @@ "zjJZBd": "You're ready!", "zonsdq": "Failed to load LNURL service", "zvCDao": "Automatically show latest notes" -} +} \ No newline at end of file diff --git a/packages/nostr/src/legacy/Tag.ts b/packages/nostr/src/legacy/Tag.ts index d2ca31b0..9b197832 100644 --- a/packages/nostr/src/legacy/Tag.ts +++ b/packages/nostr/src/legacy/Tag.ts @@ -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; + } } } From 729cbe7cbb36822acaade01998d62fc2f5e23d8c Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 5 Apr 2023 11:58:26 +0100 Subject: [PATCH 2/4] cleanup --- packages/app/src/Element/NoteCreator.tsx | 2 +- packages/app/src/Element/NoteFooter.tsx | 9 +++- packages/app/src/LNURL.ts | 56 ++++++++++++++++++++++++ packages/app/src/Pages/ProfilePage.tsx | 20 ++++++--- packages/app/src/Util.ts | 20 --------- 5 files changed, 78 insertions(+), 29 deletions(-) diff --git a/packages/app/src/Element/NoteCreator.tsx b/packages/app/src/Element/NoteCreator.tsx index 825a7f94..2383363b 100644 --- a/packages/app/src/Element/NoteCreator.tsx +++ b/packages/app/src/Element/NoteCreator.tsx @@ -58,6 +58,7 @@ export function NoteCreator(props: NoteCreatorProps) { try { const svc = new LNURL(zapForward); await svc.load(); + extraTags = [svc.getZapTag()]; } catch { setError( formatMessage({ @@ -66,7 +67,6 @@ export function NoteCreator(props: NoteCreatorProps) { ); return; } - extraTags = [["zap", zapForward]]; } const ev = replyTo ? await publisher.reply(replyTo, note, extraTags) : await publisher.note(note, extraTags); console.debug("Sending note: ", ev); diff --git a/packages/app/src/Element/NoteFooter.tsx b/packages/app/src/Element/NoteFooter.tsx index f11e7cdb..cf7a35c8 100644 --- a/packages/app/src/Element/NoteFooter.tsx +++ b/packages/app/src/Element/NoteFooter.tsx @@ -154,11 +154,16 @@ export default function NoteFooter(props: NoteFooterProps) { } function getLNURL() { - return ev.Tags.find(a => a.Key === "zap")?.LNURL || author?.lud16 || author?.lud06; + return ev.tags.find(a => a[0] === "zap")?.[1] || author?.lud16 || author?.lud06; } function getTargetName() { - return ev.Tags.find(a => a.Key === "zap")?.LNURL || author?.display_name || author?.name; + 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) { diff --git a/packages/app/src/LNURL.ts b/packages/app/src/LNURL.ts index 94b3485b..2f80f692 100644 --- a/packages/app/src/LNURL.ts +++ b/packages/app/src/LNURL.ts @@ -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) { diff --git a/packages/app/src/Pages/ProfilePage.tsx b/packages/app/src/Pages/ProfilePage.tsx index 4c9500bf..970c8904 100644 --- a/packages/app/src/Pages/ProfilePage.tsx +++ b/packages/app/src/Pages/ProfilePage.tsx @@ -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 && (
setShowLnQr(true)}> - {lnurl} + {lnurl.name}
)} setShowLnQr(false)} author={id} diff --git a/packages/app/src/Util.ts b/packages/app/src/Util.ts index 1172cfc3..dbb277fe 100644 --- a/packages/app/src/Util.ts +++ b/packages/app/src/Util.ts @@ -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); } From b6b3485225aaa0d73d46d5ccabc6047d06658d8b Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 5 Apr 2023 12:06:07 +0100 Subject: [PATCH 3/4] Move toggle preview to advanced menu --- packages/app/src/Element/Modal.css | 1 + packages/app/src/Element/NoteCreator.tsx | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/app/src/Element/Modal.css b/packages/app/src/Element/Modal.css index cbbeeca9..4ce97359 100644 --- a/packages/app/src/Element/Modal.css +++ b/packages/app/src/Element/Modal.css @@ -9,6 +9,7 @@ justify-content: center; align-items: center; z-index: 42; + overflow-y: auto; } .modal-body { diff --git a/packages/app/src/Element/NoteCreator.tsx b/packages/app/src/Element/NoteCreator.tsx index 2383363b..34b65919 100644 --- a/packages/app/src/Element/NoteCreator.tsx +++ b/packages/app/src/Element/NoteCreator.tsx @@ -111,6 +111,9 @@ export function NoteCreator(props: NoteCreatorProps) { function cancel() { setShow(false); setNote(""); + setShowAdvanced(false); + setPreview(undefined); + setZapForward(""); } function onSubmit(ev: React.MouseEvent) { @@ -168,9 +171,6 @@ export function NoteCreator(props: NoteCreatorProps) { )}
- @@ -182,7 +182,10 @@ export function NoteCreator(props: NoteCreatorProps) {
{showAdvanced && ( - <> +
+

@@ -198,7 +201,7 @@ export function NoteCreator(props: NoteCreatorProps) { value={zapForward} onChange={e => setZapForward(e.target.value)} /> - +
)} )} From b95ad17009a5c192b1dfd70e1f0d130f4ed272ec Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 5 Apr 2023 12:09:21 +0100 Subject: [PATCH 4/4] formatting --- packages/app/src/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 8bb7e76b..01e73d67 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -325,4 +325,4 @@ "zjJZBd": "You're ready!", "zonsdq": "Failed to load LNURL service", "zvCDao": "Automatically show latest notes" -} \ No newline at end of file +}