Use NostrLink everywhere

This commit is contained in:
Kieran 2023-09-19 09:30:01 +01:00
parent a1cd56292a
commit 9fb6f0dfee
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
24 changed files with 164 additions and 220 deletions

View File

@ -1,13 +1,17 @@
import { NostrLink } from "@snort/system";
import { useArticles } from "Feed/ArticlesFeed"; import { useArticles } from "Feed/ArticlesFeed";
import { orderDescending } from "SnortUtils"; import { orderDescending } from "SnortUtils";
import Note from "../Note"; import Note from "../Note";
import { useReactions } from "Feed/FeedReactions";
export default function Articles() { export default function Articles() {
const data = useArticles(); const data = useArticles();
const related = useReactions("articles:reactions", data.data?.map(v => NostrLink.fromEvent(v)) ?? []);
return ( return (
<> <>
{orderDescending(data.data ?? []).map(a => ( {orderDescending(data.data ?? []).map(a => (
<Note data={a} key={a.id} related={[]} /> <Note data={a} key={a.id} related={related.data ?? []} />
))} ))}
</> </>
); );

View File

@ -1,11 +1,10 @@
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system"; import { NostrEvent, NostrLink } from "@snort/system";
import { findTag, unwrap } from "SnortUtils"; import { findTag } from "SnortUtils";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
export function LiveEvent({ ev }: { ev: NostrEvent }) { export function LiveEvent({ ev }: { ev: NostrEvent }) {
const title = findTag(ev, "title"); const title = findTag(ev, "title");
const d = unwrap(findTag(ev, "d"));
return ( return (
<div className="text"> <div className="text">
<div className="flex card"> <div className="flex card">
@ -13,7 +12,7 @@ export function LiveEvent({ ev }: { ev: NostrEvent }) {
<h3>{title}</h3> <h3>{title}</h3>
</div> </div>
<div> <div>
<Link to={`https://zap.stream/${encodeTLV(NostrPrefix.Address, d, undefined, ev.kind, ev.pubkey)}`}> <Link to={`https://zap.stream/${NostrLink.fromEvent(ev).encode()}`}>
<button className="primary" type="button"> <button className="primary" type="button">
<FormattedMessage defaultMessage="Watch Live!" /> <FormattedMessage defaultMessage="Watch Live!" />
</button> </button>

View File

@ -1,5 +1,5 @@
import "./LiveStreams.css"; import "./LiveStreams.css";
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system"; import { NostrEvent, NostrLink } from "@snort/system";
import { findTag } from "SnortUtils"; import { findTag } from "SnortUtils";
import { CSSProperties, useMemo } from "react"; import { CSSProperties, useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@ -32,7 +32,7 @@ function LiveStreamEvent({ ev }: { ev: NostrEvent }) {
const image = findTag(ev, "image"); const image = findTag(ev, "image");
const status = findTag(ev, "status"); 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 ?? ""); const imageProxy = proxy(image ?? "");
return ( return (

View File

@ -11,8 +11,7 @@ import {
Lists, Lists,
EventExt, EventExt,
parseZap, parseZap,
tagToNostrLink, NostrLink
createNostrLinkToEvent,
} from "@snort/system"; } from "@snort/system";
import { System } from "index"; import { System } from "index";
@ -47,9 +46,9 @@ import Reactions from "Element/Reactions";
import { ZapGoal } from "Element/ZapGoal"; import { ZapGoal } from "Element/ZapGoal";
import NoteReaction from "Element/NoteReaction"; import NoteReaction from "Element/NoteReaction";
import ProfilePreview from "Element/ProfilePreview"; import ProfilePreview from "Element/ProfilePreview";
import { ProxyImg } from "Element/ProxyImg";
import messages from "./messages"; import messages from "./messages";
import { ProxyImg } from "./ProxyImg";
export interface NoteProps { export interface NoteProps {
data: TaggedNostrEvent; data: TaggedNostrEvent;
@ -299,7 +298,7 @@ export function NoteInner(props: NoteProps) {
return; return;
} }
const link = createNostrLinkToEvent(eTarget); const link = NostrLink.fromEvent(eTarget);
// detect cmd key and open in new tab // detect cmd key and open in new tab
if (e.metaKey) { if (e.metaKey) {
window.open(`/e/${link.encode()}`, "_blank"); window.open(`/e/${link.encode()}`, "_blank");
@ -319,7 +318,7 @@ export function NoteInner(props: NoteProps) {
const maxMentions = 2; const maxMentions = 2;
const replyTo = thread?.replyTo ?? thread?.root; const replyTo = thread?.replyTo ?? thread?.root;
const replyLink = replyTo const replyLink = replyTo
? tagToNostrLink( ? NostrLink.fromTag(
[replyTo.key, replyTo.value ?? "", replyTo.relay ?? "", replyTo.marker ?? ""].filter(a => a.length > 0), [replyTo.key, replyTo.value ?? "", replyTo.relay ?? "", replyTo.marker ?? ""].filter(a => a.length > 0),
) )
: undefined; : undefined;

View File

@ -1,5 +1,5 @@
import { FormattedMessage, useIntl } from "react-intl"; 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 { Menu, MenuItem } from "@szhsin/react-menu";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
@ -56,7 +56,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
} }
async function share() { 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}`; 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({
@ -92,7 +92,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
} }
async function copyId() { async function copyId() {
const link = encodeTLV(NostrPrefix.Event, ev.id, ev.relays); const link = NostrLink.fromEvent(ev).encode();
await navigator.clipboard.writeText(link); await navigator.clipboard.writeText(link);
} }

View File

@ -1,7 +1,7 @@
import "./NoteCreator.css"; import "./NoteCreator.css";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useDispatch, useSelector } from "react-redux"; 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 Icon from "Icons/Icon";
import useEventPublisher from "Feed/EventPublisher"; import useEventPublisher from "Feed/EventPublisher";
@ -172,7 +172,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(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(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

@ -2,7 +2,7 @@ import React, { HTMLProps, useContext, useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { useLongPress } from "use-long-press"; 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 { SnortContext, useUserProfile } from "@snort/system-react";
import { formatShort } from "Number"; import { formatShort } from "Number";
@ -120,7 +120,7 @@ export default function NoteFooter(props: NoteFooterProps) {
name: getDisplayName(author, ev.pubkey), name: getDisplayName(author, ev.pubkey),
zap: { zap: {
pubkey: ev.pubkey, pubkey: ev.pubkey,
event: createNostrLinkToEvent(ev), event: NostrLink.fromEvent(ev),
}, },
} as ZapTarget, } as ZapTarget,
]; ];

View File

@ -1,7 +1,7 @@
import "./Timeline.css"; import "./Timeline.css";
import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react"; import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
import { FormattedMessage } from "react-intl"; 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 { unixNow } from "@snort/shared";
import { SnortContext } from "@snort/system-react"; import { SnortContext } from "@snort/system-react";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
@ -36,7 +36,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
); );
const reactions = useReactions( const reactions = useReactions(
"follows-feed-reactions", "follows-feed-reactions",
feed.map(a => a.id), feed.map(a => NostrLink.fromEvent(a)),
); );
const system = useContext(SnortContext); const system = useContext(SnortContext);
const login = useLogin(); const login = useLogin();

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { NostrEvent, TaggedNostrEvent } from "@snort/system"; import { NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
import PageSpinner from "Element/PageSpinner"; import PageSpinner from "Element/PageSpinner";
import Note from "Element/Note"; import Note from "Element/Note";
@ -8,7 +8,7 @@ import { useReactions } from "Feed/FeedReactions";
export default function TrendingNotes() { export default function TrendingNotes() {
const [posts, setPosts] = useState<Array<NostrEvent>>(); const [posts, setPosts] = useState<Array<NostrEvent>>();
const related = useReactions("trending", posts?.map(a => a.id) ?? []); const related = useReactions("trending", posts?.map(a => NostrLink.fromEvent(a)) ?? []);
async function loadTrendingNotes() { async function loadTrendingNotes() {
const api = new NostrBandApi(); const api = new NostrBandApi();

View File

@ -1,4 +1,4 @@
import { encodeTLV, NostrPrefix, NostrEvent } from "@snort/system"; import { NostrPrefix, NostrEvent, NostrLink } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher"; import useEventPublisher from "Feed/EventPublisher";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import Spinner from "Icons/Spinner"; import Spinner from "Icons/Spinner";
@ -37,7 +37,7 @@ export default function WriteMessage({ chat }: { chat: Chat }) {
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(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}`); setMsg(`${msg ? `${msg}\n` : ""}${link}`);
setOtherEvents([...otherEvents, rx.header]); setOtherEvents([...otherEvents, rx.header]);
} else if (rx.url) { } else if (rx.url) {

View File

@ -1,6 +1,6 @@
import "./ZapGoal.css"; import "./ZapGoal.css";
import { CSSProperties, useState } from "react"; import { CSSProperties, useState } from "react";
import { NostrEvent, NostrPrefix, createNostrLink } from "@snort/system"; import { NostrEvent, NostrLink } from "@snort/system";
import useZapsFeed from "Feed/ZapsFeed"; import useZapsFeed from "Feed/ZapsFeed";
import { formatShort } from "Number"; import { formatShort } from "Number";
import { findTag } from "SnortUtils"; import { findTag } from "SnortUtils";
@ -10,7 +10,7 @@ import { Zapper } from "Zapper";
export function ZapGoal({ ev }: { ev: NostrEvent }) { export function ZapGoal({ ev }: { ev: NostrEvent }) {
const [zap, setZap] = useState(false); 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 target = Number(findTag(ev, "amount"));
const amount = zaps.reduce((acc, v) => (acc += v.amount * 1000), 0); const amount = zaps.reduce((acc, v) => (acc += v.amount * 1000), 0);
const progress = 100 * (amount / target); const progress = 100 * (amount / target);

View File

@ -1,6 +1,6 @@
import "./ZapstrEmbed.css"; import "./ZapstrEmbed.css";
import { Link } from "react-router-dom"; 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 { ProxyImg } from "Element/ProxyImg";
import ProfileImage from "Element/ProfileImage"; 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 subject = ev.tags.find(a => a[0] === "subject");
const refPersons = ev.tags.filter(a => a[0] === "p"); const refPersons = ev.tags.filter(a => a[0] === "p");
const link = encodeTLV( const link = NostrLink.fromEvent(ev).encode();
NostrPrefix.Address,
ev.tags.find(a => a[0] === "d")?.[1] ?? "",
undefined,
ev.kind,
ev.pubkey,
);
return ( return (
<> <>
<div className="flex zapstr mb10 card"> <div className="flex zapstr mb10 card">

View File

@ -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 { useRequestBuilder } from "@snort/system-react";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { useMemo } from "react"; import { useMemo } from "react";
export function useReactions(subId: string, ids: Array<string>, others?: (rb: RequestBuilder) => void) { export function useReactions(subId: string, ids: Array<NostrLink>, others?: (rb: RequestBuilder) => void) {
const { preferences: pref } = useLogin(); const { preferences: pref } = useLogin();
const sub = useMemo(() => { const sub = useMemo(() => {
const rb = new RequestBuilder(subId); const rb = new RequestBuilder(subId);
if (ids.length > 0) { const eTags = ids.filter(a => a.type === NostrPrefix.Note || a.type === NostrPrefix.Event);
rb.withFilter() const aTags = ids.filter(a => a.type === NostrPrefix.Address);
if (aTags.length > 0 || eTags.length > 0) {
const f = rb.withFilter()
.kinds( .kinds(
pref.enableReactions pref.enableReactions
? [EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt] ? [EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt]
: [EventKind.ZapReceipt, EventKind.Repost], : [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); others?.(rb);
return rb.numFilters > 0 ? rb : null; return rb.numFilters > 0 ? rb : null;

View File

@ -3,11 +3,9 @@ import { EventKind, NoteCollection, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { unixNow } from "@snort/shared"; import { unixNow } from "@snort/shared";
import { unwrap, tagFilterOfTextRepost } from "SnortUtils";
import useTimelineWindow from "Hooks/useTimelineWindow"; import useTimelineWindow from "Hooks/useTimelineWindow";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { SearchRelays } from "Const"; import { SearchRelays } from "Const";
import { useReactions } from "./FeedReactions";
export interface TimelineFeedOptions { export interface TimelineFeedOptions {
method: "TIME_RANGE" | "LIMIT_UNTIL"; method: "TIME_RANGE" | "LIMIT_UNTIL";
@ -140,36 +138,9 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
latest.clear(); latest.clear();
}, [subject.relay]); }, [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 { return {
main: main.data, main: main.data,
related: related.data, related: [],
latest: latest.data, latest: latest.data,
loading: main.loading(), loading: main.loading(),
loadMore: () => { loadMore: () => {

View File

@ -1,7 +1,6 @@
import { unwrap } from "@snort/shared"; import { unwrap } from "@snort/shared";
import { import {
EventExt, EventExt,
EventKind,
NostrLink, NostrLink,
NostrPrefix, NostrPrefix,
TaggedNostrEvent, TaggedNostrEvent,
@ -32,8 +31,7 @@ export function ThreadContextWrapper({ link, children }: { link: NostrLink; chil
const chains = new Map<u256, Array<TaggedNostrEvent>>(); const chains = new Map<u256, Array<TaggedNostrEvent>>();
if (thread.data) { if (thread.data) {
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 => { .forEach(v => {
const t = EventExt.extractThread(v); const t = EventExt.extractThread(v);
let replyTo = t?.replyTo?.value ?? t?.root?.value; let replyTo = t?.replyTo?.value ?? t?.root?.value;

View File

@ -2,7 +2,7 @@ import "./Deck.css";
import { CSSProperties, createContext, useContext, useEffect, useState } from "react"; import { CSSProperties, createContext, useContext, useEffect, useState } from "react";
import { Outlet, useNavigate } from "react-router-dom"; import { Outlet, useNavigate } from "react-router-dom";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { NostrPrefix, createNostrLink } from "@snort/system"; import { NostrLink } from "@snort/system";
import { DeckNav } from "Element/Deck/Nav"; import { DeckNav } from "Element/Deck/Nav";
import useLoginFeed from "Feed/LoginFeed"; import useLoginFeed from "Feed/LoginFeed";
@ -25,8 +25,8 @@ import useLogin from "Hooks/useLogin";
type Cols = "notes" | "articles" | "media" | "streams" | "notifications"; type Cols = "notes" | "articles" | "media" | "streams" | "notifications";
interface DeckScope { interface DeckScope {
thread?: string; thread?: NostrLink,
setThread: (e?: string) => void; setThread: (e?: NostrLink) => void
} }
export const DeckContext = createContext<DeckScope | undefined>(undefined); export const DeckContext = createContext<DeckScope | undefined>(undefined);
@ -35,7 +35,7 @@ export function SnortDeckLayout() {
const login = useLogin(); const login = useLogin();
const navigate = useNavigate(); const navigate = useNavigate();
const [deckScope, setDeckScope] = useState<DeckScope>({ const [deckScope, setDeckScope] = useState<DeckScope>({
setThread: (e?: string) => setDeckScope(s => ({ ...s, thread: e })), setThread: (e?: NostrLink) => setDeckScope(s => ({ ...s, thread: e }))
}); });
useLoginFeed(); useLoginFeed();
@ -71,7 +71,7 @@ export function SnortDeckLayout() {
{deckScope.thread && ( {deckScope.thread && (
<> <>
<Modal onClose={() => deckScope.setThread(undefined)} className="thread-overlay"> <Modal onClose={() => deckScope.setThread(undefined)} className="thread-overlay">
<ThreadContextWrapper link={createNostrLink(NostrPrefix.Note, deckScope.thread)}> <ThreadContextWrapper link={deckScope.thread}>
<SpotlightFromThread onClose={() => deckScope.setThread(undefined)} /> <SpotlightFromThread onClose={() => deckScope.setThread(undefined)} />
<div> <div>
<Thread onBack={() => deckScope.setThread(undefined)} /> <Thread onBack={() => 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(); const { proxy } = useImgProxy();
return ( return (
<div> <div>
@ -158,7 +158,7 @@ function MediaCol({ setThread }: { setThread: (e: string) => void }) {
"--img": `url(${proxy(images[0].content)})`, "--img": `url(${proxy(images[0].content)})`,
} as CSSProperties } as CSSProperties
} }
onClick={() => setThread(e.id)}></div> onClick={() => setThread(NostrLink.fromEvent(e))}></div>
); );
}} }}
/> />

View File

@ -4,7 +4,7 @@ import { useDispatch, useSelector } from "react-redux";
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"; import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
import { NostrPrefix, createNostrLink, tryParseNostrLink } from "@snort/system"; import { NostrLink, NostrPrefix, tryParseNostrLink } from "@snort/system";
import messages from "./messages"; import messages from "./messages";
@ -125,7 +125,7 @@ const AccountHeader = () => {
const [handle, domain] = search.split("@"); const [handle, domain] = search.split("@");
const pk = await fetchNip05Pubkey(handle, domain); const pk = await fetchNip05Pubkey(handle, domain);
if (pk) { if (pk) {
navigate(`/${createNostrLink(NostrPrefix.PublicKey, pk).encode()}`); navigate(`/${new NostrLink(NostrPrefix.PublicKey, pk).encode()}`);
return; return;
} }
} }

View File

@ -7,7 +7,6 @@ import {
NostrLink, NostrLink,
NostrPrefix, NostrPrefix,
TaggedNostrEvent, TaggedNostrEvent,
createNostrLink,
parseZap, parseZap,
} from "@snort/system"; } from "@snort/system";
import { unwrap } from "@snort/shared"; import { unwrap } from "@snort/shared";
@ -33,15 +32,15 @@ function notificationContext(ev: TaggedNostrEvent) {
const aTag = findTag(ev, "a"); const aTag = findTag(ev, "a");
if (aTag) { if (aTag) {
const [kind, author, d] = aTag.split(":"); 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"); const eTag = findTag(ev, "e");
if (eTag) { 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]; const pTag = ev.tags.filter(a => a[0] === "p").slice(-1)?.[0];
if (pTag) { if (pTag) {
return createNostrLink(NostrPrefix.PublicKey, pTag[1]); return new NostrLink(NostrPrefix.PublicKey, pTag[1]);
} }
break; break;
} }
@ -50,16 +49,16 @@ function notificationContext(ev: TaggedNostrEvent) {
const thread = EventExt.extractThread(ev); const thread = EventExt.extractThread(ev);
const tag = unwrap(thread?.replyTo ?? thread?.root ?? { value: ev.id, key: "e" }); const tag = unwrap(thread?.replyTo ?? thread?.root ?? { value: ev.id, key: "e" });
if (tag.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") { } else if (tag.key === "a") {
const [kind, author, d] = unwrap(tag.value).split(":"); 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 { } else {
throw new Error("Unknown thread context"); throw new Error("Unknown thread context");
} }
} }
case EventKind.TextNote: { case EventKind.TextNote: {
return createNostrLink(NostrPrefix.Note, ev.id); return new NostrLink(NostrPrefix.Note, ev.id);
} }
} }
} }

View File

@ -3,11 +3,11 @@ import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { import {
createNostrLink,
encodeTLV, encodeTLV,
encodeTLVEntries, encodeTLVEntries,
EventKind, EventKind,
HexKey, HexKey,
NostrLink,
NostrPrefix, NostrPrefix,
TLVEntryType, TLVEntryType,
tryParseNostrLink, tryParseNostrLink,
@ -70,7 +70,7 @@ const RELAYS = 7;
const BOOKMARKS = 8; const BOOKMARKS = 8;
function ZapsProfileTab({ id }: { id: HexKey }) { 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); const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
return ( return (
<div className="main-content"> <div className="main-content">

View File

@ -2,6 +2,7 @@ import { useContext, useEffect, useState } from "react";
import { Link, Outlet, RouteObject, useParams } from "react-router-dom"; import { Link, Outlet, RouteObject, useParams } from "react-router-dom";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { unixNow } from "@snort/shared"; import { unixNow } from "@snort/shared";
import { NostrLink } from "@snort/system";
import Timeline from "Element/Timeline"; import Timeline from "Element/Timeline";
import { System } from "index"; import { System } from "index";
@ -141,16 +142,9 @@ export const NotesTab = () => {
<> <>
<FollowsHint /> <FollowsHint />
<TaskList /> <TaskList />
<TimelineFollows <TimelineFollows postsOnly={true} noteOnClick={deckContext ? (ev) => {
postsOnly={true} deckContext.setThread(NostrLink.fromEvent(ev));
noteOnClick={ } : undefined} />
deckContext
? ev => {
deckContext.setThread(ev.id);
}
: undefined
}
/>
</> </>
); );
}; };

View File

@ -3,9 +3,7 @@ import {
EventPublisher, EventPublisher,
NostrEvent, NostrEvent,
NostrLink, NostrLink,
SystemInterface, SystemInterface
createNostrLinkToEvent,
linkToEventTag,
} from "@snort/system"; } from "@snort/system";
import { generateRandomKey } from "Login"; import { generateRandomKey } from "Login";
import { isHex } from "SnortUtils"; import { isHex } from "SnortUtils";
@ -63,7 +61,7 @@ export class Zapper {
weight: Number(v[3] ?? 0), weight: Number(v[3] ?? 0),
zap: { zap: {
pubkey: v[1], pubkey: v[1],
event: createNostrLinkToEvent(ev), event: NostrLink.fromEvent(ev),
}, },
} as ZapTarget; } as ZapTarget;
} else { } else {
@ -74,7 +72,7 @@ export class Zapper {
weight: 1, weight: 1,
zap: { zap: {
pubkey: ev.pubkey, pubkey: ev.pubkey,
event: createNostrLinkToEvent(ev), event: NostrLink.fromEvent(ev),
}, },
} as ZapTarget; } as ZapTarget;
} }
@ -103,7 +101,7 @@ export class Zapper {
t.zap && svc.canZap t.zap && svc.canZap
? await pub?.zap(toSend * 1000, t.zap.pubkey, relays, undefined, t.memo, eb => { ? await pub?.zap(toSend * 1000, t.zap.pubkey, relays, undefined, t.memo, eb => {
if (t.zap?.event) { if (t.zap?.event) {
const tag = linkToEventTag(t.zap.event); const tag = t.zap.event.toEventTag();
if (tag) { if (tag) {
eb.tag(tag); eb.tag(tag);
} }

View File

@ -2,7 +2,7 @@ import * as utils from "@noble/curves/abstract/utils";
import { bech32 } from "@scure/base"; import { bech32 } from "@scure/base";
import { HexKey } from "./nostr"; import { HexKey } from "./nostr";
export enum NostrPrefix { export const enum NostrPrefix {
PublicKey = "npub", PublicKey = "npub",
PrivateKey = "nsec", PrivateKey = "nsec",
Note = "note", Note = "note",

View File

@ -2,82 +2,73 @@ import { bech32ToHex, hexToBech32, unwrap } from "@snort/shared";
import { NostrPrefix, decodeTLV, TLVEntryType, encodeTLV, NostrEvent, TaggedNostrEvent } from "."; import { NostrPrefix, decodeTLV, TLVEntryType, encodeTLV, NostrEvent, TaggedNostrEvent } from ".";
import { findTag } from "./utils"; import { findTag } from "./utils";
export interface NostrLink { export class NostrLink {
type: NostrPrefix; constructor(
id: string; readonly type: NostrPrefix,
kind?: number; readonly id: string,
author?: string; readonly kind?: number,
relays?: Array<string>; readonly author?: string,
encode(): string; readonly relays?: Array<string>
} ) { }
export function linkToEventTag(link: NostrLink) { encode(): string {
const relayEntry = link.relays ? [link.relays[0]] : []; if(this.type === NostrPrefix.Note || this.type === NostrPrefix.PrivateKey || this.type === NostrPrefix.PublicKey) {
if (link.type === NostrPrefix.PublicKey) { return hexToBech32(this.type, this.id);
return ["p", link.id]; } else {
} else if (link.type === NostrPrefix.Note || link.type === NostrPrefix.Event) { return encodeTLV(this.type, this.id, this.relays, this.kind, this.author);
return ["e", link.id];
} else if (link.type === NostrPrefix.Address) {
return ["a", `${link.kind}:${link.author}:${link.id}`, ...relayEntry];
}
}
export function tagToNostrLink(tag: Array<string>) {
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);
} }
} }
throw new Error(`Unknown tag kind ${tag[0]}`);
}
export function createNostrLinkToEvent(ev: TaggedNostrEvent | NostrEvent) { toEventTag() {
const relays = "relays" in ev ? ev.relays : undefined; const relayEntry = this.relays ? [this.relays[0]] : [];
if (this.type === NostrPrefix.PublicKey) {
if (ev.kind >= 30_000 && ev.kind < 40_000) { return ["p", this.id];
const dTag = unwrap(findTag(ev, "d")); } else if (this.type === NostrPrefix.Note || this.type === NostrPrefix.Event) {
return createNostrLink(NostrPrefix.Address, dTag, relays, ev.kind, ev.pubkey); return ["e", this.id, ...relayEntry];
} } else if (this.type === NostrPrefix.Address) {
return createNostrLink(NostrPrefix.Event, ev.id, relays, ev.kind, ev.pubkey); return ["a", `${this.kind}:${this.author}:${this.id}`, ...relayEntry];
}
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;
} }
} else if (link.type === NostrPrefix.Event || link.type === NostrPrefix.Note) {
return link.id === ev.id;
} }
return false; matchesEvent(ev: NostrEvent) {
} if (this.type === NostrPrefix.Address) {
const dTag = findTag(ev, "d");
export function createNostrLink(prefix: NostrPrefix, id: string, relays?: string[], kind?: number, author?: string) { if (dTag && dTag === this.id && unwrap(this.author) === ev.pubkey && unwrap(this.kind) === ev.kind) {
return { return true;
type: prefix,
id,
relays,
kind,
author,
encode: () => {
if (prefix === NostrPrefix.Note || prefix === NostrPrefix.PublicKey) {
return hexToBech32(prefix, id);
} }
if (prefix === NostrPrefix.Address || prefix === NostrPrefix.Event || prefix === NostrPrefix.Profile) { } else if (this.type === NostrPrefix.Event || this.type === NostrPrefix.Note) {
return encodeTLV(prefix, id, relays, kind, author); return this.id === ev.id;
}
return false;
}
static fromTag(tag: Array<string>) {
const relays = tag.length > 2 ? [tag[2]]: undefined;
switch (tag[0]) {
case "e": {
return new NostrLink(NostrPrefix.Event, tag[1], undefined, undefined, relays);
} }
return ""; case "p": {
}, return new NostrLink(NostrPrefix.Profile, tag[1], undefined, undefined, relays);
} as NostrLink; }
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 { export function validateNostrLink(link: string): boolean {
@ -114,19 +105,11 @@ export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLin
if (isPrefix(NostrPrefix.PublicKey)) { if (isPrefix(NostrPrefix.PublicKey)) {
const id = bech32ToHex(entity); const id = bech32ToHex(entity);
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
return { return new NostrLink(NostrPrefix.PublicKey, id);
type: NostrPrefix.PublicKey,
id: id,
encode: () => hexToBech32(NostrPrefix.PublicKey, id),
};
} else if (isPrefix(NostrPrefix.Note)) { } else if (isPrefix(NostrPrefix.Note)) {
const id = bech32ToHex(entity); const id = bech32ToHex(entity);
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
return { return new NostrLink(NostrPrefix.Note, id);
type: NostrPrefix.Note,
id: id,
encode: () => hexToBech32(NostrPrefix.Note, id),
};
} else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) { } else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) {
const decoded = decodeTLV(entity); 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 author = decoded.find(a => a.type === TLVEntryType.Author)?.value as string;
const kind = decoded.find(a => a.type === TLVEntryType.Kind)?.value as number; const kind = decoded.find(a => a.type === TLVEntryType.Kind)?.value as number;
const encode = () => {
return entity; // return original
};
if (isPrefix(NostrPrefix.Profile)) { if (isPrefix(NostrPrefix.Profile)) {
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
return { return new NostrLink(NostrPrefix.Profile, id, kind, author, relays);
type: NostrPrefix.Profile,
id,
relays,
kind,
author,
encode,
};
} else if (isPrefix(NostrPrefix.Event)) { } else if (isPrefix(NostrPrefix.Event)) {
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
return { return new NostrLink(NostrPrefix.Event, id, kind, author, relays);
type: NostrPrefix.Event,
id,
relays,
kind,
author,
encode,
};
} else if (isPrefix(NostrPrefix.Address)) { } else if (isPrefix(NostrPrefix.Address)) {
return { return new NostrLink(NostrPrefix.Address, id, kind, author, relays);
type: NostrPrefix.Address,
id,
relays,
kind,
author,
encode,
};
} }
} else if (prefixHint) { } else if (prefixHint) {
return { return new NostrLink(prefixHint, link);
type: prefixHint,
id: link,
encode: () => hexToBech32(prefixHint, link),
};
} }
throw new Error("Invalid nostr link"); throw new Error("Invalid nostr link");
} }

View File

@ -1,9 +1,9 @@
import debug from "debug"; import debug from "debug";
import { v4 as uuid } from "uuid"; 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 EventKind from "./event-kind";
import { SystemInterface } from "index"; import { NostrLink, NostrPrefix, SystemInterface } from "index";
import { ReqFilter, u256, HexKey } from "./nostr"; import { ReqFilter, u256, HexKey } from "./nostr";
import { RelayCache, splitByWriteRelays, splitFlatByWriteRelays } from "./gossip-model"; import { RelayCache, splitByWriteRelays, splitFlatByWriteRelays } from "./gossip-model";
@ -229,6 +229,30 @@ export class RequestFilterBuilder {
return this; 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 * Build/expand this filter into a set of relay specific queries
*/ */