Use NostrLink everywhere
This commit is contained in:
parent
a1cd56292a
commit
9fb6f0dfee
@ -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 => (
|
||||
<Note data={a} key={a.id} related={[]} />
|
||||
<Note data={a} key={a.id} related={related.data ?? []} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -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 (
|
||||
<div className="text">
|
||||
<div className="flex card">
|
||||
@ -13,7 +12,7 @@ export function LiveEvent({ ev }: { ev: NostrEvent }) {
|
||||
<h3>{title}</h3>
|
||||
</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">
|
||||
<FormattedMessage defaultMessage="Watch Live!" />
|
||||
</button>
|
||||
|
@ -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 (
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
];
|
||||
|
@ -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();
|
||||
|
@ -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<Array<NostrEvent>>();
|
||||
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();
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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 (
|
||||
<>
|
||||
<div className="flex zapstr mb10 card">
|
||||
|
@ -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<string>, others?: (rb: RequestBuilder) => void) {
|
||||
export function useReactions(subId: string, ids: Array<NostrLink>, 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;
|
||||
|
@ -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: () => {
|
||||
|
@ -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<u256, Array<TaggedNostrEvent>>();
|
||||
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;
|
||||
|
@ -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<DeckScope | undefined>(undefined);
|
||||
@ -35,7 +35,7 @@ export function SnortDeckLayout() {
|
||||
const login = useLogin();
|
||||
const navigate = useNavigate();
|
||||
const [deckScope, setDeckScope] = useState<DeckScope>({
|
||||
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 && (
|
||||
<>
|
||||
<Modal onClose={() => deckScope.setThread(undefined)} className="thread-overlay">
|
||||
<ThreadContextWrapper link={createNostrLink(NostrPrefix.Note, deckScope.thread)}>
|
||||
<ThreadContextWrapper link={deckScope.thread}>
|
||||
<SpotlightFromThread onClose={() => deckScope.setThread(undefined)} />
|
||||
<div>
|
||||
<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();
|
||||
return (
|
||||
<div>
|
||||
@ -158,7 +158,7 @@ function MediaCol({ setThread }: { setThread: (e: string) => void }) {
|
||||
"--img": `url(${proxy(images[0].content)})`,
|
||||
} as CSSProperties
|
||||
}
|
||||
onClick={() => setThread(e.id)}></div>
|
||||
onClick={() => setThread(NostrLink.fromEvent(e))}></div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<div className="main-content">
|
||||
|
@ -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 = () => {
|
||||
<>
|
||||
<FollowsHint />
|
||||
<TaskList />
|
||||
<TimelineFollows
|
||||
postsOnly={true}
|
||||
noteOnClick={
|
||||
deckContext
|
||||
? ev => {
|
||||
deckContext.setThread(ev.id);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<TimelineFollows postsOnly={true} noteOnClick={deckContext ? (ev) => {
|
||||
deckContext.setThread(NostrLink.fromEvent(ev));
|
||||
} : undefined} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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<string>;
|
||||
encode(): string;
|
||||
}
|
||||
export class NostrLink {
|
||||
constructor(
|
||||
readonly type: NostrPrefix,
|
||||
readonly id: string,
|
||||
readonly kind?: number,
|
||||
readonly author?: string,
|
||||
readonly relays?: Array<string>
|
||||
) { }
|
||||
|
||||
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<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);
|
||||
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<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 "";
|
||||
},
|
||||
} 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");
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user