feat: render embed for zapstr replies

This commit is contained in:
Kieran 2023-05-15 13:52:29 +01:00
parent 323d4e761e
commit d8fc92fafc
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
10 changed files with 33 additions and 39 deletions

View File

@ -2,10 +2,8 @@ import { Link } from "react-router-dom";
import { EventKind, NostrPrefix } from "@snort/nostr"; import { EventKind, NostrPrefix } from "@snort/nostr";
import Mention from "Element/Mention"; import Mention from "Element/Mention";
import NostrFileHeader from "Element/NostrFileHeader";
import { parseNostrLink } from "Util"; import { parseNostrLink } from "Util";
import NoteQuote from "Element/NoteQuote"; import NoteQuote from "Element/NoteQuote";
import ZapstrEmbed from "Element/ZapstrEmbed";
export default function NostrLink({ link, depth }: { link: string; depth?: number }) { export default function NostrLink({ link, depth }: { link: string; depth?: number }) {
const nav = parseNostrLink(link); const nav = parseNostrLink(link);
@ -13,13 +11,6 @@ export default function NostrLink({ link, depth }: { link: string; depth?: numbe
if (nav?.type === NostrPrefix.PublicKey || nav?.type === NostrPrefix.Profile) { if (nav?.type === NostrPrefix.PublicKey || nav?.type === NostrPrefix.Profile) {
return <Mention pubkey={nav.id} relays={nav.relays} />; return <Mention pubkey={nav.id} relays={nav.relays} />;
} else if (nav?.type === NostrPrefix.Note || nav?.type === NostrPrefix.Event || nav?.type === NostrPrefix.Address) { } else if (nav?.type === NostrPrefix.Note || nav?.type === NostrPrefix.Event || nav?.type === NostrPrefix.Address) {
if (nav.kind === EventKind.FileHeader) {
return <NostrFileHeader link={nav} />;
}
if (nav.kind === 31337) {
return <ZapstrEmbed link={nav} />;
}
if ((depth ?? 0) > 0) { if ((depth ?? 0) > 0) {
const evLink = nav.encode(); const evLink = nav.encode();
return ( return (

View File

@ -30,6 +30,7 @@ import { EventExt } from "System/EventExt";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { setBookmarked, setPinned } from "Login"; import { setBookmarked, setPinned } from "Login";
import { NostrFileElement } from "Element/NostrFileHeader"; import { NostrFileElement } from "Element/NostrFileHeader";
import ZapstrEmbed from "Element/ZapstrEmbed";
import messages from "./messages"; import messages from "./messages";
@ -78,7 +79,9 @@ export default function Note(props: NoteProps) {
if (ev.kind === EventKind.FileHeader) { if (ev.kind === EventKind.FileHeader) {
return <NostrFileElement ev={ev} />; return <NostrFileElement ev={ev} />;
} }
if (ev.kind === 31337) {
return <ZapstrEmbed ev={ev} />;
}
const navigate = useNavigate(); const navigate = useNavigate();
const [showReactions, setShowReactions] = useState(false); const [showReactions, setShowReactions] = useState(false);
const deletions = useMemo(() => getReactions(related, ev.id, EventKind.Deletion), [related]); const deletions = useMemo(() => getReactions(related, ev.id, EventKind.Deletion), [related]);

View File

@ -142,7 +142,7 @@ export function NoteCreator() {
if (file) { if (file) {
const rx = await uploader.upload(file, file.name); const rx = await uploader.upload(file, file.name);
if (rx.header) { if (rx.header) {
const link = `nostr:${encodeTLV(rx.header.id, NostrPrefix.Event, undefined, rx.header.kind)}`; const link = `nostr:${encodeTLV(NostrPrefix.Event, rx.header.id, undefined, rx.header.kind)}`;
dispatch(setNote(`${note ? `${note}\n` : ""}${link}`)); dispatch(setNote(`${note ? `${note}\n` : ""}${link}`));
dispatch(setOtherEvents([...otherEvents, rx.header])); dispatch(setOtherEvents([...otherEvents, rx.header]));
} else if (rx.url) { } else if (rx.url) {

View File

@ -264,7 +264,7 @@ export default function NoteFooter(props: NoteFooterProps) {
} }
async function share() { async function share() {
const link = encodeTLV(ev.id, NostrPrefix.Event, ev.relays); const link = encodeTLV(NostrPrefix.Event, ev.id, ev.relays);
const url = `${window.location.protocol}//${window.location.host}/e/${link}`; const url = `${window.location.protocol}//${window.location.host}/e/${link}`;
if ("share" in window.navigator) { if ("share" in window.navigator) {
await window.navigator.share({ await window.navigator.share({
@ -300,7 +300,7 @@ export default function NoteFooter(props: NoteFooterProps) {
} }
async function copyId() { async function copyId() {
const link = encodeTLV(ev.id, NostrPrefix.Event, ev.relays); const link = encodeTLV(NostrPrefix.Event, ev.id, ev.relays);
await navigator.clipboard.writeText(link); await navigator.clipboard.writeText(link);
} }

View File

@ -35,7 +35,7 @@ export default function WriteDm({ chatPubKey }: { chatPubKey: string }) {
if (file) { if (file) {
const rx = await uploader.upload(file, file.name); const rx = await uploader.upload(file, file.name);
if (rx.header) { if (rx.header) {
const link = `nostr:${encodeTLV(rx.header.id, NostrPrefix.Event, undefined, rx.header.kind)}`; const link = `nostr:${encodeTLV(NostrPrefix.Event, rx.header.id, undefined, rx.header.kind)}`;
setMsg(`${msg ? `${msg}\n` : ""}${link}`); setMsg(`${msg ? `${msg}\n` : ""}${link}`);
setOtherEvents([...otherEvents, rx.header]); setOtherEvents([...otherEvents, rx.header]);
} else if (rx.url) { } else if (rx.url) {

View File

@ -1,25 +1,27 @@
import "./ZapstrEmbed.css"; import "./ZapstrEmbed.css";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { encodeTLV, NostrPrefix, RawEvent } from "@snort/nostr";
import useEventFeed from "Feed/EventFeed";
import Spinner from "Icons/Spinner";
import { NostrLink } from "Util";
import { ProxyImg } from "Element/ProxyImg"; import { ProxyImg } from "Element/ProxyImg";
import ProfileImage from "Element/ProfileImage"; import ProfileImage from "Element/ProfileImage";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
export default function ZapstrEmbed({ link }: { link: NostrLink }) { export default function ZapstrEmbed({ ev }: { ev: RawEvent }) {
const ev = useEventFeed(link); const media = ev.tags.find(a => a[0] === "media");
const cover = ev.tags.find(a => a[0] === "cover");
const subject = ev.tags.find(a => a[0] === "subject");
const refPersons = ev.tags.filter(a => a[0] === "p");
if (!ev.data) return <Spinner />; const link = encodeTLV(
NostrPrefix.Address,
const media = ev.data.tags.find(a => a[0] === "media"); ev.tags.find(a => a[0] === "d")?.[1] ?? "",
const cover = ev.data.tags.find(a => a[0] === "cover"); undefined,
const subject = ev.data.tags.find(a => a[0] === "subject"); ev.kind,
const refPersons = ev.data.tags.filter(a => a[0] === "p"); ev.pubkey
);
return ( return (
<> <>
<div className="flex zapstr mb10"> <div className="flex zapstr mb10 card">
<ProxyImg src={cover?.[1] ?? ""} size={100} /> <ProxyImg src={cover?.[1] ?? ""} size={100} />
<div className="flex f-col"> <div className="flex f-col">
<div> <div>
@ -33,7 +35,7 @@ export default function ZapstrEmbed({ link }: { link: NostrLink }) {
</div> </div>
</div> </div>
</div> </div>
<Link to={`https://zapstr.live/?track=${link.encode()}`} target="_blank"> <Link to={`https://zapstr.live/?track=${link}`} target="_blank">
<button> <button>
<FormattedMessage defaultMessage="Open on Zapstr" /> <FormattedMessage defaultMessage="Open on Zapstr" />
</button> </button>

View File

@ -296,7 +296,7 @@ export default function ProfilePage() {
function renderIcons() { function renderIcons() {
if (!id) return; if (!id) return;
const link = encodeTLV(id, NostrPrefix.Profile); const link = encodeTLV(NostrPrefix.Profile, id);
return ( return (
<div className="icon-actions"> <div className="icon-actions">
<IconButton onClick={() => setShowProfileQr(true)}> <IconButton onClick={() => setShowProfileQr(true)}>

View File

@ -15,7 +15,7 @@ export default function ExportKeys() {
<FormattedMessage defaultMessage="Public Key" /> <FormattedMessage defaultMessage="Public Key" />
</h3> </h3>
<Copy text={hexToBech32("npub", publicKey ?? "")} maxSize={48} className="mb10" /> <Copy text={hexToBech32("npub", publicKey ?? "")} maxSize={48} className="mb10" />
<Copy text={encodeTLV(publicKey ?? "", NostrPrefix.Profile)} maxSize={48} /> <Copy text={encodeTLV(NostrPrefix.Profile, publicKey ?? "")} maxSize={48} />
{privateKey && ( {privateKey && (
<> <>
<h3> <h3>

View File

@ -91,7 +91,7 @@ export function bech32ToText(str: string) {
*/ */
export function eventLink(hex: u256, relays?: Array<string> | string) { export function eventLink(hex: u256, relays?: Array<string> | string) {
const encoded = relays const encoded = relays
? encodeTLV(hex, NostrPrefix.Event, Array.isArray(relays) ? relays : [relays]) ? encodeTLV(NostrPrefix.Event, hex, Array.isArray(relays) ? relays : [relays])
: hexToBech32(NostrPrefix.Note, hex); : hexToBech32(NostrPrefix.Note, hex);
return `/e/${encoded}`; return `/e/${encoded}`;
} }
@ -101,7 +101,7 @@ export function eventLink(hex: u256, relays?: Array<string> | string) {
*/ */
export function profileLink(hex: HexKey, relays?: Array<string> | string) { export function profileLink(hex: HexKey, relays?: Array<string> | string) {
const encoded = relays const encoded = relays
? encodeTLV(hex, NostrPrefix.Profile, Array.isArray(relays) ? relays : [relays]) ? encodeTLV(NostrPrefix.Event, hex, Array.isArray(relays) ? relays : [relays])
: hexToBech32(NostrPrefix.PublicKey, hex); : hexToBech32(NostrPrefix.PublicKey, hex);
return `/p/${encoded}`; return `/p/${encoded}`;
} }
@ -119,7 +119,7 @@ export function hexToBech32(hrp: string, hex?: string) {
const buf = secp.utils.hexToBytes(hex); const buf = secp.utils.hexToBytes(hex);
return bech32.encode(hrp, bech32.toWords(buf)); return bech32.encode(hrp, bech32.toWords(buf));
} else { } else {
return encodeTLV(hex, hrp as NostrPrefix); return encodeTLV(hrp as NostrPrefix, hex);
} }
} catch (e) { } catch (e) {
console.warn("Invalid hex", hex, e); console.warn("Invalid hex", hex, e);

View File

@ -27,13 +27,9 @@ export interface TLVEntry {
value: string | HexKey | number; value: string | HexKey | number;
} }
export function encodeTLV(hex: string, prefix: NostrPrefix, relays?: string[], kind?: number) { export function encodeTLV(prefix: NostrPrefix, id: string, relays?: string[], kind?: number, author?: string) {
if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) {
return "";
}
const enc = new TextEncoder(); const enc = new TextEncoder();
const buf = secp.utils.hexToBytes(hex); const buf = prefix === NostrPrefix.Address ? enc.encode(id) : secp.utils.hexToBytes(id);
const tl0 = [0, buf.length, ...buf]; const tl0 = [0, buf.length, ...buf];
const tl1 = const tl1 =
@ -43,9 +39,11 @@ export function encodeTLV(hex: string, prefix: NostrPrefix, relays?: string[], k
return [1, data.length, ...data]; return [1, data.length, ...data];
}) })
.flat() ?? []; .flat() ?? [];
const tl2 = author ? [2, 32, ...secp.utils.hexToBytes(author)] : [];
const tl3 = kind ? [3, 4, ...new Uint8Array(new Uint32Array([kind]).buffer).reverse()] : [] const tl3 = kind ? [3, 4, ...new Uint8Array(new Uint32Array([kind]).buffer).reverse()] : []
return bech32.encode(prefix, bech32.toWords([...tl0, ...tl1, ...tl3]), 1_000); return bech32.encode(prefix, bech32.toWords([...tl0, ...tl1, ...tl2, ...tl3]), 1_000);
} }
export function decodeTLV(str: string) { export function decodeTLV(str: string) {