From 9fb6f0dfee08d2e5d6e22e5a4a1d85307a22a74a Mon Sep 17 00:00:00 2001 From: Kieran Date: Tue, 19 Sep 2023 09:30:01 +0100 Subject: [PATCH] Use NostrLink everywhere --- packages/app/src/Element/Deck/Articles.tsx | 6 +- packages/app/src/Element/LiveEvent.tsx | 7 +- packages/app/src/Element/LiveStreams.tsx | 4 +- packages/app/src/Element/Note.tsx | 9 +- packages/app/src/Element/NoteContextMenu.tsx | 6 +- packages/app/src/Element/NoteCreator.tsx | 4 +- packages/app/src/Element/NoteFooter.tsx | 4 +- packages/app/src/Element/TimelineFollows.tsx | 4 +- packages/app/src/Element/TrendingPosts.tsx | 4 +- packages/app/src/Element/WriteMessage.tsx | 4 +- packages/app/src/Element/ZapGoal.tsx | 4 +- packages/app/src/Element/ZapstrEmbed.tsx | 10 +- packages/app/src/Feed/FeedReactions.ts | 21 ++- packages/app/src/Feed/TimelineFeed.ts | 31 +--- packages/app/src/Hooks/useThreadContext.tsx | 4 +- packages/app/src/Pages/DeckLayout.tsx | 14 +- packages/app/src/Pages/Layout.tsx | 4 +- packages/app/src/Pages/Notifications.tsx | 13 +- packages/app/src/Pages/ProfilePage.tsx | 4 +- packages/app/src/Pages/Root.tsx | 14 +- packages/app/src/Zapper.ts | 10 +- packages/system/src/links.ts | 2 +- packages/system/src/nostr-link.ts | 173 +++++++------------ packages/system/src/request-builder.ts | 28 ++- 24 files changed, 164 insertions(+), 220 deletions(-) diff --git a/packages/app/src/Element/Deck/Articles.tsx b/packages/app/src/Element/Deck/Articles.tsx index e80e0c96..10efb1bc 100644 --- a/packages/app/src/Element/Deck/Articles.tsx +++ b/packages/app/src/Element/Deck/Articles.tsx @@ -1,13 +1,17 @@ +import { NostrLink } from "@snort/system"; import { useArticles } from "Feed/ArticlesFeed"; import { orderDescending } from "SnortUtils"; import Note from "../Note"; +import { useReactions } from "Feed/FeedReactions"; export default function Articles() { const data = useArticles(); + const related = useReactions("articles:reactions", data.data?.map(v => NostrLink.fromEvent(v)) ?? []); + return ( <> {orderDescending(data.data ?? []).map(a => ( - + ))} ); diff --git a/packages/app/src/Element/LiveEvent.tsx b/packages/app/src/Element/LiveEvent.tsx index d64ed907..0300feda 100644 --- a/packages/app/src/Element/LiveEvent.tsx +++ b/packages/app/src/Element/LiveEvent.tsx @@ -1,11 +1,10 @@ -import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system"; -import { findTag, unwrap } from "SnortUtils"; +import { NostrEvent, NostrLink } from "@snort/system"; +import { findTag } from "SnortUtils"; import { FormattedMessage } from "react-intl"; import { Link } from "react-router-dom"; export function LiveEvent({ ev }: { ev: NostrEvent }) { const title = findTag(ev, "title"); - const d = unwrap(findTag(ev, "d")); return (
@@ -13,7 +12,7 @@ export function LiveEvent({ ev }: { ev: NostrEvent }) {

{title}

- + diff --git a/packages/app/src/Element/LiveStreams.tsx b/packages/app/src/Element/LiveStreams.tsx index f639a2de..20e2a2e2 100644 --- a/packages/app/src/Element/LiveStreams.tsx +++ b/packages/app/src/Element/LiveStreams.tsx @@ -1,5 +1,5 @@ import "./LiveStreams.css"; -import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system"; +import { NostrEvent, NostrLink } from "@snort/system"; import { findTag } from "SnortUtils"; import { CSSProperties, useMemo } from "react"; import { Link } from "react-router-dom"; @@ -32,7 +32,7 @@ function LiveStreamEvent({ ev }: { ev: NostrEvent }) { const image = findTag(ev, "image"); const status = findTag(ev, "status"); - const link = encodeTLV(NostrPrefix.Address, findTag(ev, "d") ?? "", undefined, ev.kind, ev.pubkey); + const link = NostrLink.fromEvent(ev).encode(); const imageProxy = proxy(image ?? ""); return ( diff --git a/packages/app/src/Element/Note.tsx b/packages/app/src/Element/Note.tsx index 84ee88ea..40bcb8ee 100644 --- a/packages/app/src/Element/Note.tsx +++ b/packages/app/src/Element/Note.tsx @@ -11,8 +11,7 @@ import { Lists, EventExt, parseZap, - tagToNostrLink, - createNostrLinkToEvent, + NostrLink } from "@snort/system"; import { System } from "index"; @@ -47,9 +46,9 @@ import Reactions from "Element/Reactions"; import { ZapGoal } from "Element/ZapGoal"; import NoteReaction from "Element/NoteReaction"; import ProfilePreview from "Element/ProfilePreview"; +import { ProxyImg } from "Element/ProxyImg"; import messages from "./messages"; -import { ProxyImg } from "./ProxyImg"; export interface NoteProps { data: TaggedNostrEvent; @@ -299,7 +298,7 @@ export function NoteInner(props: NoteProps) { return; } - const link = createNostrLinkToEvent(eTarget); + const link = NostrLink.fromEvent(eTarget); // detect cmd key and open in new tab if (e.metaKey) { window.open(`/e/${link.encode()}`, "_blank"); @@ -319,7 +318,7 @@ export function NoteInner(props: NoteProps) { const maxMentions = 2; const replyTo = thread?.replyTo ?? thread?.root; const replyLink = replyTo - ? tagToNostrLink( + ? NostrLink.fromTag( [replyTo.key, replyTo.value ?? "", replyTo.relay ?? "", replyTo.marker ?? ""].filter(a => a.length > 0), ) : undefined; diff --git a/packages/app/src/Element/NoteContextMenu.tsx b/packages/app/src/Element/NoteContextMenu.tsx index c3c81b90..acfbad96 100644 --- a/packages/app/src/Element/NoteContextMenu.tsx +++ b/packages/app/src/Element/NoteContextMenu.tsx @@ -1,5 +1,5 @@ import { FormattedMessage, useIntl } from "react-intl"; -import { HexKey, Lists, NostrPrefix, TaggedNostrEvent, encodeTLV } from "@snort/system"; +import { HexKey, Lists, NostrLink, TaggedNostrEvent } from "@snort/system"; import { Menu, MenuItem } from "@szhsin/react-menu"; import { useDispatch, useSelector } from "react-redux"; @@ -56,7 +56,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) { } async function share() { - const link = encodeTLV(NostrPrefix.Event, ev.id, ev.relays); + const link = NostrLink.fromEvent(ev).encode(); const url = `${window.location.protocol}//${window.location.host}/e/${link}`; if ("share" in window.navigator) { await window.navigator.share({ @@ -92,7 +92,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) { } async function copyId() { - const link = encodeTLV(NostrPrefix.Event, ev.id, ev.relays); + const link = NostrLink.fromEvent(ev).encode(); await navigator.clipboard.writeText(link); } diff --git a/packages/app/src/Element/NoteCreator.tsx b/packages/app/src/Element/NoteCreator.tsx index 6d47764b..33cd1b4c 100644 --- a/packages/app/src/Element/NoteCreator.tsx +++ b/packages/app/src/Element/NoteCreator.tsx @@ -1,7 +1,7 @@ import "./NoteCreator.css"; import { FormattedMessage, useIntl } from "react-intl"; import { useDispatch, useSelector } from "react-redux"; -import { encodeTLV, EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink } from "@snort/system"; +import { EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink, NostrLink } from "@snort/system"; import Icon from "Icons/Icon"; import useEventPublisher from "Feed/EventPublisher"; @@ -172,7 +172,7 @@ export function NoteCreator() { if (file) { const rx = await uploader.upload(file, file.name); if (rx.header) { - const link = `nostr:${encodeTLV(NostrPrefix.Event, rx.header.id, undefined, rx.header.kind)}`; + const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode()}`; dispatch(setNote(`${note ? `${note}\n` : ""}${link}`)); dispatch(setOtherEvents([...otherEvents, rx.header])); } else if (rx.url) { diff --git a/packages/app/src/Element/NoteFooter.tsx b/packages/app/src/Element/NoteFooter.tsx index 9f757c88..90bd8eea 100644 --- a/packages/app/src/Element/NoteFooter.tsx +++ b/packages/app/src/Element/NoteFooter.tsx @@ -2,7 +2,7 @@ import React, { HTMLProps, useContext, useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { useIntl } from "react-intl"; import { useLongPress } from "use-long-press"; -import { TaggedNostrEvent, ParsedZap, countLeadingZeros, createNostrLinkToEvent } from "@snort/system"; +import { TaggedNostrEvent, ParsedZap, countLeadingZeros, NostrLink } from "@snort/system"; import { SnortContext, useUserProfile } from "@snort/system-react"; import { formatShort } from "Number"; @@ -120,7 +120,7 @@ export default function NoteFooter(props: NoteFooterProps) { name: getDisplayName(author, ev.pubkey), zap: { pubkey: ev.pubkey, - event: createNostrLinkToEvent(ev), + event: NostrLink.fromEvent(ev), }, } as ZapTarget, ]; diff --git a/packages/app/src/Element/TimelineFollows.tsx b/packages/app/src/Element/TimelineFollows.tsx index e7f95698..cb57a2e5 100644 --- a/packages/app/src/Element/TimelineFollows.tsx +++ b/packages/app/src/Element/TimelineFollows.tsx @@ -1,7 +1,7 @@ import "./Timeline.css"; import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react"; import { FormattedMessage } from "react-intl"; -import { TaggedNostrEvent, EventKind, u256, NostrEvent } from "@snort/system"; +import { TaggedNostrEvent, EventKind, u256, NostrEvent, NostrLink } from "@snort/system"; import { unixNow } from "@snort/shared"; import { SnortContext } from "@snort/system-react"; import { useInView } from "react-intersection-observer"; @@ -36,7 +36,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => { ); const reactions = useReactions( "follows-feed-reactions", - feed.map(a => a.id), + feed.map(a => NostrLink.fromEvent(a)), ); const system = useContext(SnortContext); const login = useLogin(); diff --git a/packages/app/src/Element/TrendingPosts.tsx b/packages/app/src/Element/TrendingPosts.tsx index b3e0d52b..c6a5e565 100644 --- a/packages/app/src/Element/TrendingPosts.tsx +++ b/packages/app/src/Element/TrendingPosts.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { NostrEvent, TaggedNostrEvent } from "@snort/system"; +import { NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system"; import PageSpinner from "Element/PageSpinner"; import Note from "Element/Note"; @@ -8,7 +8,7 @@ import { useReactions } from "Feed/FeedReactions"; export default function TrendingNotes() { const [posts, setPosts] = useState>(); - const related = useReactions("trending", posts?.map(a => a.id) ?? []); + const related = useReactions("trending", posts?.map(a => NostrLink.fromEvent(a)) ?? []); async function loadTrendingNotes() { const api = new NostrBandApi(); diff --git a/packages/app/src/Element/WriteMessage.tsx b/packages/app/src/Element/WriteMessage.tsx index 1a3c5941..ee1feea4 100644 --- a/packages/app/src/Element/WriteMessage.tsx +++ b/packages/app/src/Element/WriteMessage.tsx @@ -1,4 +1,4 @@ -import { encodeTLV, NostrPrefix, NostrEvent } from "@snort/system"; +import { NostrPrefix, NostrEvent, NostrLink } from "@snort/system"; import useEventPublisher from "Feed/EventPublisher"; import Icon from "Icons/Icon"; import Spinner from "Icons/Spinner"; @@ -37,7 +37,7 @@ export default function WriteMessage({ chat }: { chat: Chat }) { if (file) { const rx = await uploader.upload(file, file.name); if (rx.header) { - const link = `nostr:${encodeTLV(NostrPrefix.Event, rx.header.id, undefined, rx.header.kind)}`; + const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode()}`; setMsg(`${msg ? `${msg}\n` : ""}${link}`); setOtherEvents([...otherEvents, rx.header]); } else if (rx.url) { diff --git a/packages/app/src/Element/ZapGoal.tsx b/packages/app/src/Element/ZapGoal.tsx index 9b4759bc..97cd972e 100644 --- a/packages/app/src/Element/ZapGoal.tsx +++ b/packages/app/src/Element/ZapGoal.tsx @@ -1,6 +1,6 @@ import "./ZapGoal.css"; import { CSSProperties, useState } from "react"; -import { NostrEvent, NostrPrefix, createNostrLink } from "@snort/system"; +import { NostrEvent, NostrLink } from "@snort/system"; import useZapsFeed from "Feed/ZapsFeed"; import { formatShort } from "Number"; import { findTag } from "SnortUtils"; @@ -10,7 +10,7 @@ import { Zapper } from "Zapper"; export function ZapGoal({ ev }: { ev: NostrEvent }) { const [zap, setZap] = useState(false); - const zaps = useZapsFeed(createNostrLink(NostrPrefix.Note, ev.id)); + const zaps = useZapsFeed(NostrLink.fromEvent(ev)); const target = Number(findTag(ev, "amount")); const amount = zaps.reduce((acc, v) => (acc += v.amount * 1000), 0); const progress = 100 * (amount / target); diff --git a/packages/app/src/Element/ZapstrEmbed.tsx b/packages/app/src/Element/ZapstrEmbed.tsx index fa112e8a..184254c5 100644 --- a/packages/app/src/Element/ZapstrEmbed.tsx +++ b/packages/app/src/Element/ZapstrEmbed.tsx @@ -1,6 +1,6 @@ import "./ZapstrEmbed.css"; import { Link } from "react-router-dom"; -import { encodeTLV, NostrPrefix, NostrEvent } from "@snort/system"; +import { NostrEvent, NostrLink } from "@snort/system"; import { ProxyImg } from "Element/ProxyImg"; import ProfileImage from "Element/ProfileImage"; @@ -12,13 +12,7 @@ export default function ZapstrEmbed({ ev }: { ev: NostrEvent }) { const subject = ev.tags.find(a => a[0] === "subject"); const refPersons = ev.tags.filter(a => a[0] === "p"); - const link = encodeTLV( - NostrPrefix.Address, - ev.tags.find(a => a[0] === "d")?.[1] ?? "", - undefined, - ev.kind, - ev.pubkey, - ); + const link = NostrLink.fromEvent(ev).encode(); return ( <>
diff --git a/packages/app/src/Feed/FeedReactions.ts b/packages/app/src/Feed/FeedReactions.ts index 5faaeb00..c9625ab9 100644 --- a/packages/app/src/Feed/FeedReactions.ts +++ b/packages/app/src/Feed/FeedReactions.ts @@ -1,21 +1,30 @@ -import { RequestBuilder, EventKind, NoteCollection } from "@snort/system"; +import { RequestBuilder, EventKind, NoteCollection, NostrLink, NostrPrefix } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import useLogin from "Hooks/useLogin"; import { useMemo } from "react"; -export function useReactions(subId: string, ids: Array, others?: (rb: RequestBuilder) => void) { +export function useReactions(subId: string, ids: Array, others?: (rb: RequestBuilder) => void) { const { preferences: pref } = useLogin(); const sub = useMemo(() => { const rb = new RequestBuilder(subId); - if (ids.length > 0) { - rb.withFilter() + const eTags = ids.filter(a => a.type === NostrPrefix.Note || a.type === NostrPrefix.Event); + const aTags = ids.filter(a => a.type === NostrPrefix.Address); + + if (aTags.length > 0 || eTags.length > 0) { + const f = rb.withFilter() .kinds( pref.enableReactions ? [EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt] : [EventKind.ZapReceipt, EventKind.Repost], - ) - .tag("e", ids); + ); + + if(aTags.length > 0) { + f.tag("a", aTags.map(v => `${v.kind}:${v.author}:${v.id}`)); + } + if(eTags.length > 0) { + f.tag("e", eTags.map(v => v.id)); + } } others?.(rb); return rb.numFilters > 0 ? rb : null; diff --git a/packages/app/src/Feed/TimelineFeed.ts b/packages/app/src/Feed/TimelineFeed.ts index 6feb29da..f566168e 100644 --- a/packages/app/src/Feed/TimelineFeed.ts +++ b/packages/app/src/Feed/TimelineFeed.ts @@ -3,11 +3,9 @@ import { EventKind, NoteCollection, RequestBuilder } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import { unixNow } from "@snort/shared"; -import { unwrap, tagFilterOfTextRepost } from "SnortUtils"; import useTimelineWindow from "Hooks/useTimelineWindow"; import useLogin from "Hooks/useLogin"; import { SearchRelays } from "Const"; -import { useReactions } from "./FeedReactions"; export interface TimelineFeedOptions { method: "TIME_RANGE" | "LIMIT_UNTIL"; @@ -140,36 +138,9 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel latest.clear(); }, [subject.relay]); - function getParentEvents() { - if (main.data) { - const repostsByKind6 = main.data - .filter(a => a.kind === EventKind.Repost && a.content === "") - .map(a => a.tags.find(b => b[0] === "e")) - .filter(a => a) - .map(a => unwrap(a)[1]); - const repostsByKind1 = main.data - .filter( - a => (a.kind === EventKind.Repost || a.kind === EventKind.TextNote) && a.tags.some(tagFilterOfTextRepost(a)), - ) - .map(a => a.tags.find(tagFilterOfTextRepost(a))) - .filter(a => a) - .map(a => unwrap(a)[1]); - return [...repostsByKind6, ...repostsByKind1]; - } - return []; - } - - const trackingEvents = main.data?.map(a => a.id) ?? []; - const related = useReactions(`timeline-related:${subject.type}:${subject.discriminator}`, trackingEvents, rb => { - const trackingParentEvents = getParentEvents(); - if (trackingParentEvents.length > 0) { - rb.withFilter().ids(trackingParentEvents); - } - }); - return { main: main.data, - related: related.data, + related: [], latest: latest.data, loading: main.loading(), loadMore: () => { diff --git a/packages/app/src/Hooks/useThreadContext.tsx b/packages/app/src/Hooks/useThreadContext.tsx index 9ff86792..0d11130d 100644 --- a/packages/app/src/Hooks/useThreadContext.tsx +++ b/packages/app/src/Hooks/useThreadContext.tsx @@ -1,7 +1,6 @@ import { unwrap } from "@snort/shared"; import { EventExt, - EventKind, NostrLink, NostrPrefix, TaggedNostrEvent, @@ -32,8 +31,7 @@ export function ThreadContextWrapper({ link, children }: { link: NostrLink; chil const chains = new Map>(); if (thread.data) { thread.data - ?.filter(a => a.kind === EventKind.TextNote) - .sort((a, b) => b.created_at - a.created_at) + ?.sort((a, b) => b.created_at - a.created_at) .forEach(v => { const t = EventExt.extractThread(v); let replyTo = t?.replyTo?.value ?? t?.root?.value; diff --git a/packages/app/src/Pages/DeckLayout.tsx b/packages/app/src/Pages/DeckLayout.tsx index 92c98e54..45a5e230 100644 --- a/packages/app/src/Pages/DeckLayout.tsx +++ b/packages/app/src/Pages/DeckLayout.tsx @@ -2,7 +2,7 @@ import "./Deck.css"; import { CSSProperties, createContext, useContext, useEffect, useState } from "react"; import { Outlet, useNavigate } from "react-router-dom"; import { FormattedMessage } from "react-intl"; -import { NostrPrefix, createNostrLink } from "@snort/system"; +import { NostrLink } from "@snort/system"; import { DeckNav } from "Element/Deck/Nav"; import useLoginFeed from "Feed/LoginFeed"; @@ -25,8 +25,8 @@ import useLogin from "Hooks/useLogin"; type Cols = "notes" | "articles" | "media" | "streams" | "notifications"; interface DeckScope { - thread?: string; - setThread: (e?: string) => void; + thread?: NostrLink, + setThread: (e?: NostrLink) => void } export const DeckContext = createContext(undefined); @@ -35,7 +35,7 @@ export function SnortDeckLayout() { const login = useLogin(); const navigate = useNavigate(); const [deckScope, setDeckScope] = useState({ - setThread: (e?: string) => setDeckScope(s => ({ ...s, thread: e })), + setThread: (e?: NostrLink) => setDeckScope(s => ({ ...s, thread: e })) }); useLoginFeed(); @@ -71,7 +71,7 @@ export function SnortDeckLayout() { {deckScope.thread && ( <> deckScope.setThread(undefined)} className="thread-overlay"> - + deckScope.setThread(undefined)} />
deckScope.setThread(undefined)} /> @@ -128,7 +128,7 @@ function ArticlesCol() { ); } -function MediaCol({ setThread }: { setThread: (e: string) => void }) { +function MediaCol({ setThread }: { setThread: (e: NostrLink) => void }) { const { proxy } = useImgProxy(); return (
@@ -158,7 +158,7 @@ function MediaCol({ setThread }: { setThread: (e: string) => void }) { "--img": `url(${proxy(images[0].content)})`, } as CSSProperties } - onClick={() => setThread(e.id)}>
+ onClick={() => setThread(NostrLink.fromEvent(e))}>
); }} /> diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx index 10864cde..4de52fde 100644 --- a/packages/app/src/Pages/Layout.tsx +++ b/packages/app/src/Pages/Layout.tsx @@ -4,7 +4,7 @@ import { useDispatch, useSelector } from "react-redux"; import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; import { useUserProfile } from "@snort/system-react"; -import { NostrPrefix, createNostrLink, tryParseNostrLink } from "@snort/system"; +import { NostrLink, NostrPrefix, tryParseNostrLink } from "@snort/system"; import messages from "./messages"; @@ -125,7 +125,7 @@ const AccountHeader = () => { const [handle, domain] = search.split("@"); const pk = await fetchNip05Pubkey(handle, domain); if (pk) { - navigate(`/${createNostrLink(NostrPrefix.PublicKey, pk).encode()}`); + navigate(`/${new NostrLink(NostrPrefix.PublicKey, pk).encode()}`); return; } } diff --git a/packages/app/src/Pages/Notifications.tsx b/packages/app/src/Pages/Notifications.tsx index 23cefcd7..70de123e 100644 --- a/packages/app/src/Pages/Notifications.tsx +++ b/packages/app/src/Pages/Notifications.tsx @@ -7,7 +7,6 @@ import { NostrLink, NostrPrefix, TaggedNostrEvent, - createNostrLink, parseZap, } from "@snort/system"; import { unwrap } from "@snort/shared"; @@ -33,15 +32,15 @@ function notificationContext(ev: TaggedNostrEvent) { const aTag = findTag(ev, "a"); if (aTag) { const [kind, author, d] = aTag.split(":"); - return createNostrLink(NostrPrefix.Address, d, undefined, Number(kind), author); + return new NostrLink(NostrPrefix.Address, d, Number(kind), author); } const eTag = findTag(ev, "e"); if (eTag) { - return createNostrLink(NostrPrefix.Event, eTag); + return new NostrLink(NostrPrefix.Event, eTag); } const pTag = ev.tags.filter(a => a[0] === "p").slice(-1)?.[0]; if (pTag) { - return createNostrLink(NostrPrefix.PublicKey, pTag[1]); + return new NostrLink(NostrPrefix.PublicKey, pTag[1]); } break; } @@ -50,16 +49,16 @@ function notificationContext(ev: TaggedNostrEvent) { const thread = EventExt.extractThread(ev); const tag = unwrap(thread?.replyTo ?? thread?.root ?? { value: ev.id, key: "e" }); if (tag.key === "e") { - return createNostrLink(NostrPrefix.Event, unwrap(tag.value)); + return new NostrLink(NostrPrefix.Event, unwrap(tag.value)); } else if (tag.key === "a") { const [kind, author, d] = unwrap(tag.value).split(":"); - return createNostrLink(NostrPrefix.Address, d, undefined, Number(kind), author); + return new NostrLink(NostrPrefix.Address, d, Number(kind), author); } else { throw new Error("Unknown thread context"); } } case EventKind.TextNote: { - return createNostrLink(NostrPrefix.Note, ev.id); + return new NostrLink(NostrPrefix.Note, ev.id); } } } diff --git a/packages/app/src/Pages/ProfilePage.tsx b/packages/app/src/Pages/ProfilePage.tsx index 45769a91..905c4620 100644 --- a/packages/app/src/Pages/ProfilePage.tsx +++ b/packages/app/src/Pages/ProfilePage.tsx @@ -3,11 +3,11 @@ import { useEffect, useState } from "react"; import { FormattedMessage } from "react-intl"; import { useNavigate, useParams } from "react-router-dom"; import { - createNostrLink, encodeTLV, encodeTLVEntries, EventKind, HexKey, + NostrLink, NostrPrefix, TLVEntryType, tryParseNostrLink, @@ -70,7 +70,7 @@ const RELAYS = 7; const BOOKMARKS = 8; function ZapsProfileTab({ id }: { id: HexKey }) { - const zaps = useZapsFeed(createNostrLink(NostrPrefix.PublicKey, id)); + const zaps = useZapsFeed(new NostrLink(NostrPrefix.PublicKey, id)); const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0); return (
diff --git a/packages/app/src/Pages/Root.tsx b/packages/app/src/Pages/Root.tsx index 09c263a2..9a599133 100644 --- a/packages/app/src/Pages/Root.tsx +++ b/packages/app/src/Pages/Root.tsx @@ -2,6 +2,7 @@ import { useContext, useEffect, useState } from "react"; import { Link, Outlet, RouteObject, useParams } from "react-router-dom"; import { FormattedMessage } from "react-intl"; import { unixNow } from "@snort/shared"; +import { NostrLink } from "@snort/system"; import Timeline from "Element/Timeline"; import { System } from "index"; @@ -141,16 +142,9 @@ export const NotesTab = () => { <> - { - deckContext.setThread(ev.id); - } - : undefined - } - /> + { + deckContext.setThread(NostrLink.fromEvent(ev)); + } : undefined} /> ); }; diff --git a/packages/app/src/Zapper.ts b/packages/app/src/Zapper.ts index a6c158e1..d89655b3 100644 --- a/packages/app/src/Zapper.ts +++ b/packages/app/src/Zapper.ts @@ -3,9 +3,7 @@ import { EventPublisher, NostrEvent, NostrLink, - SystemInterface, - createNostrLinkToEvent, - linkToEventTag, + SystemInterface } from "@snort/system"; import { generateRandomKey } from "Login"; import { isHex } from "SnortUtils"; @@ -63,7 +61,7 @@ export class Zapper { weight: Number(v[3] ?? 0), zap: { pubkey: v[1], - event: createNostrLinkToEvent(ev), + event: NostrLink.fromEvent(ev), }, } as ZapTarget; } else { @@ -74,7 +72,7 @@ export class Zapper { weight: 1, zap: { pubkey: ev.pubkey, - event: createNostrLinkToEvent(ev), + event: NostrLink.fromEvent(ev), }, } as ZapTarget; } @@ -103,7 +101,7 @@ export class Zapper { 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); + const tag = t.zap.event.toEventTag(); if (tag) { eb.tag(tag); } diff --git a/packages/system/src/links.ts b/packages/system/src/links.ts index 5a8a230c..5d8bd366 100644 --- a/packages/system/src/links.ts +++ b/packages/system/src/links.ts @@ -2,7 +2,7 @@ import * as utils from "@noble/curves/abstract/utils"; import { bech32 } from "@scure/base"; import { HexKey } from "./nostr"; -export enum NostrPrefix { +export const enum NostrPrefix { PublicKey = "npub", PrivateKey = "nsec", Note = "note", diff --git a/packages/system/src/nostr-link.ts b/packages/system/src/nostr-link.ts index b832ad8f..aedac1a7 100644 --- a/packages/system/src/nostr-link.ts +++ b/packages/system/src/nostr-link.ts @@ -2,82 +2,73 @@ import { bech32ToHex, hexToBech32, unwrap } from "@snort/shared"; import { NostrPrefix, decodeTLV, TLVEntryType, encodeTLV, NostrEvent, TaggedNostrEvent } from "."; import { findTag } from "./utils"; -export interface NostrLink { - type: NostrPrefix; - id: string; - kind?: number; - author?: string; - relays?: Array; - encode(): string; -} +export class NostrLink { + constructor( + readonly type: NostrPrefix, + readonly id: string, + readonly kind?: number, + readonly author?: string, + readonly relays?: Array + ) { } -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 tagToNostrLink(tag: Array) { - switch (tag[0]) { - case "e": { - return createNostrLink(NostrPrefix.Event, tag[1], tag.slice(2)); - } - case "p": { - return createNostrLink(NostrPrefix.Profile, tag[1], tag.slice(2)); - } - case "a": { - const [kind, author, dTag] = tag[1].split(":"); - return createNostrLink(NostrPrefix.Address, dTag, tag.slice(2), Number(kind), author); + encode(): string { + if(this.type === NostrPrefix.Note || this.type === NostrPrefix.PrivateKey || this.type === NostrPrefix.PublicKey) { + return hexToBech32(this.type, this.id); + } else { + return encodeTLV(this.type, this.id, this.relays, this.kind, this.author); } } - throw new Error(`Unknown tag kind ${tag[0]}`); -} -export function createNostrLinkToEvent(ev: TaggedNostrEvent | NostrEvent) { - const relays = "relays" in ev ? ev.relays : undefined; - - if (ev.kind >= 30_000 && ev.kind < 40_000) { - const dTag = unwrap(findTag(ev, "d")); - return createNostrLink(NostrPrefix.Address, dTag, relays, ev.kind, ev.pubkey); - } - return createNostrLink(NostrPrefix.Event, ev.id, relays, ev.kind, ev.pubkey); -} - -export function linkMatch(link: NostrLink, ev: NostrEvent) { - if (link.type === NostrPrefix.Address) { - const dTag = findTag(ev, "d"); - if (dTag && dTag === link.id && unwrap(link.author) === ev.pubkey && unwrap(link.kind) === ev.kind) { - return true; + toEventTag() { + const relayEntry = this.relays ? [this.relays[0]] : []; + if (this.type === NostrPrefix.PublicKey) { + return ["p", this.id]; + } else if (this.type === NostrPrefix.Note || this.type === NostrPrefix.Event) { + return ["e", this.id, ...relayEntry]; + } else if (this.type === NostrPrefix.Address) { + return ["a", `${this.kind}:${this.author}:${this.id}`, ...relayEntry]; } - } else if (link.type === NostrPrefix.Event || link.type === NostrPrefix.Note) { - return link.id === ev.id; } - return false; -} - -export function createNostrLink(prefix: NostrPrefix, id: string, relays?: string[], kind?: number, author?: string) { - return { - type: prefix, - id, - relays, - kind, - author, - encode: () => { - if (prefix === NostrPrefix.Note || prefix === NostrPrefix.PublicKey) { - return hexToBech32(prefix, id); + matchesEvent(ev: NostrEvent) { + if (this.type === NostrPrefix.Address) { + const dTag = findTag(ev, "d"); + if (dTag && dTag === this.id && unwrap(this.author) === ev.pubkey && unwrap(this.kind) === ev.kind) { + return true; } - if (prefix === NostrPrefix.Address || prefix === NostrPrefix.Event || prefix === NostrPrefix.Profile) { - return encodeTLV(prefix, id, relays, kind, author); + } else if (this.type === NostrPrefix.Event || this.type === NostrPrefix.Note) { + return this.id === ev.id; + } + + return false; + } + + static fromTag(tag: Array) { + const relays = tag.length > 2 ? [tag[2]]: undefined; + switch (tag[0]) { + case "e": { + return new NostrLink(NostrPrefix.Event, tag[1], undefined, undefined, relays); } - return ""; - }, - } as NostrLink; + case "p": { + return new NostrLink(NostrPrefix.Profile, tag[1], undefined, undefined, relays); + } + case "a": { + const [kind, author, dTag] = tag[1].split(":"); + return new NostrLink(NostrPrefix.Address, dTag, Number(kind), author, relays); + } + } + throw new Error(`Unknown tag kind ${tag[0]}`); + } + + static fromEvent(ev: TaggedNostrEvent | NostrEvent) { + const relays = "relays" in ev ? ev.relays : undefined; + + if (ev.kind >= 30_000 && ev.kind < 40_000) { + const dTag = unwrap(findTag(ev, "d")); + return new NostrLink(NostrPrefix.Address, dTag, ev.kind, ev.pubkey, relays); + } + return new NostrLink(NostrPrefix.Event, ev.id, ev.kind, ev.pubkey, relays); + } } export function validateNostrLink(link: string): boolean { @@ -114,19 +105,11 @@ export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLin if (isPrefix(NostrPrefix.PublicKey)) { const id = bech32ToHex(entity); if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); - return { - type: NostrPrefix.PublicKey, - id: id, - encode: () => hexToBech32(NostrPrefix.PublicKey, id), - }; + return new NostrLink(NostrPrefix.PublicKey, id); } else if (isPrefix(NostrPrefix.Note)) { const id = bech32ToHex(entity); if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); - return { - type: NostrPrefix.Note, - id: id, - encode: () => hexToBech32(NostrPrefix.Note, id), - }; + return new NostrLink(NostrPrefix.Note, id); } else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) { const decoded = decodeTLV(entity); @@ -135,45 +118,17 @@ export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLin const author = decoded.find(a => a.type === TLVEntryType.Author)?.value as string; const kind = decoded.find(a => a.type === TLVEntryType.Kind)?.value as number; - const encode = () => { - return entity; // return original - }; if (isPrefix(NostrPrefix.Profile)) { if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); - return { - type: NostrPrefix.Profile, - id, - relays, - kind, - author, - encode, - }; + return new NostrLink(NostrPrefix.Profile, id, kind, author, relays); } else if (isPrefix(NostrPrefix.Event)) { if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); - return { - type: NostrPrefix.Event, - id, - relays, - kind, - author, - encode, - }; + return new NostrLink(NostrPrefix.Event, id, kind, author, relays); } else if (isPrefix(NostrPrefix.Address)) { - return { - type: NostrPrefix.Address, - id, - relays, - kind, - author, - encode, - }; + return new NostrLink(NostrPrefix.Address, id, kind, author, relays); } } else if (prefixHint) { - return { - type: prefixHint, - id: link, - encode: () => hexToBech32(prefixHint, link), - }; + return new NostrLink(prefixHint, link); } throw new Error("Invalid nostr link"); } diff --git a/packages/system/src/request-builder.ts b/packages/system/src/request-builder.ts index b5991895..03a0538f 100644 --- a/packages/system/src/request-builder.ts +++ b/packages/system/src/request-builder.ts @@ -1,9 +1,9 @@ import debug from "debug"; import { v4 as uuid } from "uuid"; -import { appendDedupe, sanitizeRelayUrl, unixNowMs } from "@snort/shared"; +import { appendDedupe, sanitizeRelayUrl, unixNowMs, unwrap } from "@snort/shared"; import EventKind from "./event-kind"; -import { SystemInterface } from "index"; +import { NostrLink, NostrPrefix, SystemInterface } from "index"; import { ReqFilter, u256, HexKey } from "./nostr"; import { RelayCache, splitByWriteRelays, splitFlatByWriteRelays } from "./gossip-model"; @@ -229,6 +229,30 @@ export class RequestFilterBuilder { return this; } + /** + * Get event from link + */ + link(link: NostrLink) { + if(link.type === NostrPrefix.Address) { + return this.tag("d", [link.id]) + .kinds([unwrap(link.kind)]) + .authors([unwrap(link.author)]); + } else { + return this.ids([link.id]); + } + } + + /** + * Get replies to link with e/a tags + */ + replyToLink(link: NostrLink) { + if(link.type === NostrPrefix.Address) { + return this.tag("a", [`${link.kind}:${link.author}:${link.id}`]); + } else { + return this.tag("e", [link.id]); + } + } + /** * Build/expand this filter into a set of relay specific queries */