>;
+}
+
+function renderToken(t: Token | Footnotes | Footnote | FootnoteRef): ReactNode {
+ try {
+ switch (t.type) {
+ case "paragraph": {
+ return {t.tokens ? t.tokens.map(renderToken) : t.raw}
;
+ }
+ case "image": {
+ return ;
+ }
+ case "heading": {
+ switch (t.depth) {
+ case 1:
+ return {t.tokens ? t.tokens.map(renderToken) : t.raw}
;
+ case 2:
+ return {t.tokens ? t.tokens.map(renderToken) : t.raw}
;
+ case 3:
+ return {t.tokens ? t.tokens.map(renderToken) : t.raw}
;
+ case 4:
+ return {t.tokens ? t.tokens.map(renderToken) : t.raw}
;
+ case 5:
+ return {t.tokens ? t.tokens.map(renderToken) : t.raw}
;
+ case 6:
+ return {t.tokens ? t.tokens.map(renderToken) : t.raw}
;
+ }
+ throw new Error("Invalid heading");
+ }
+ case "codespan": {
+ return {t.raw}
;
+ }
+ case "code": {
+ return {t.raw}
;
+ }
+ case "br": {
+ return
;
+ }
+ case "hr": {
+ return
;
+ }
+ case "blockquote": {
+ return {t.tokens ? t.tokens.map(renderToken) : t.raw}
;
+ }
+ case "link": {
+ return (
+
+ {t.tokens ? t.tokens.map(renderToken) : t.raw}
+
+ );
+ }
+ case "list": {
+ if (t.ordered) {
+ return {t.items.map(renderToken)}
;
+ } else {
+ return {t.items.map(renderToken)}
;
+ }
+ }
+ case "list_item": {
+ return {t.tokens ? t.tokens.map(renderToken) : t.raw};
+ }
+ case "em": {
+ return {t.tokens ? t.tokens.map(renderToken) : t.raw};
+ }
+ case "del": {
+ return {t.tokens ? t.tokens.map(renderToken) : t.raw};
+ }
+ case "footnoteRef": {
+ return (
+
+
+ [{t.label}]
+
+
+ );
+ }
+ case "footnotes":
+ case "footnote": {
+ return;
+ }
+ default: {
+ if ("tokens" in t) {
+ return (t.tokens as Array).map(renderToken);
+ }
+ return t.raw;
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+export function Markdown({ content, tags = [] }: MarkdownProps) {
+ const parsed = useMemo(() => {
+ return marked.use(markedFootnote()).lexer(content);
+ }, [content, tags]);
+
+ return (
+
+ {parsed.filter(a => a.type !== "footnote" && a.type !== "footnotes").map(a => renderToken(a))}
+
+ );
+}
diff --git a/packages/app/src/Element/Event/Note.tsx b/packages/app/src/Element/Event/Note.tsx
index aba48e7c..9b54a6e5 100644
--- a/packages/app/src/Element/Event/Note.tsx
+++ b/packages/app/src/Element/Event/Note.tsx
@@ -9,6 +9,7 @@ import { ZapGoal } from "Element/Event/ZapGoal";
import NoteReaction from "Element/Event/NoteReaction";
import ProfilePreview from "Element/User/ProfilePreview";
import { NoteInner } from "./NoteInner";
+import { LongFormText } from "./LongFormText";
export interface NoteProps {
data: TaggedNostrEvent;
@@ -32,6 +33,7 @@ export interface NoteProps {
canUnbookmark?: boolean;
canClick?: boolean;
showMediaSpotlight?: boolean;
+ longFormPreview?: boolean;
};
}
@@ -58,6 +60,9 @@ export default function Note(props: NoteProps) {
if (ev.kind === (9041 as EventKind)) {
return ;
}
+ if (ev.kind === EventKind.LongFormTextNote) {
+ return ;
+ }
return ;
}
diff --git a/packages/app/src/Element/Event/NoteCreator.tsx b/packages/app/src/Element/Event/NoteCreator.tsx
index 37a207ae..4008c91a 100644
--- a/packages/app/src/Element/Event/NoteCreator.tsx
+++ b/packages/app/src/Element/Event/NoteCreator.tsx
@@ -359,6 +359,7 @@ export function NoteCreator() {
showTime: false,
canClick: false,
showMedia: false,
+ longFormPreview: true,
}}
/>
)}
diff --git a/packages/app/src/Element/Event/NoteInner.tsx b/packages/app/src/Element/Event/NoteInner.tsx
index 4fc00af5..864a7cc0 100644
--- a/packages/app/src/Element/Event/NoteInner.tsx
+++ b/packages/app/src/Element/Event/NoteInner.tsx
@@ -1,27 +1,18 @@
import { Link, useNavigate } from "react-router-dom";
-import React, { ReactNode, useMemo, useState } from "react";
-import {
- dedupeByPubkey,
- findTag,
- getReactions,
- hexToBech32,
- normalizeReaction,
- profileLink,
- Reaction,
- tagFilterOfTextRepost,
-} from "../../SnortUtils";
-import useModeration from "../../Hooks/useModeration";
+import React, { ReactNode, useState } from "react";
import { useInView } from "react-intersection-observer";
-import useLogin from "../../Hooks/useLogin";
-import useEventPublisher from "../../Hooks/useEventPublisher";
-import { NoteContextMenu, NoteTranslation } from "./NoteContextMenu";
import { FormattedMessage, useIntl } from "react-intl";
+import { EventExt, EventKind, HexKey, Lists, NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system";
+import { findTag, hexToBech32, profileLink } from "SnortUtils";
+import useModeration from "Hooks/useModeration";
+import useLogin from "Hooks/useLogin";
+import useEventPublisher from "Hooks/useEventPublisher";
+import { NoteContextMenu, NoteTranslation } from "./NoteContextMenu";
import { UserCache } from "../../Cache";
import messages from "../messages";
import { System } from "../../index";
import { setBookmarked, setPinned } from "../../Login";
import Text from "../Text";
-import { ProxyImg } from "../ProxyImg";
import Reveal from "./Reveal";
import Poll from "./Poll";
import ProfileImage from "../User/ProfileImage";
@@ -31,7 +22,7 @@ import NoteFooter from "./NoteFooter";
import Reactions from "./Reactions";
import HiddenNote from "./HiddenNote";
import { NoteProps } from "./Note";
-import { EventExt, EventKind, HexKey, Lists, NostrLink, NostrPrefix, parseZap, TaggedNostrEvent } from "@snort/system";
+import { useEventReactions } from "Hooks/useEventReactions";
export function NoteInner(props: NoteProps) {
const { data: ev, related, highlight, options: opt, ignoreModeration = false, className } = props;
@@ -39,50 +30,17 @@ export function NoteInner(props: NoteProps) {
const baseClassName = `note card${className ? ` ${className}` : ""}`;
const navigate = useNavigate();
const [showReactions, setShowReactions] = useState(false);
- const deletions = useMemo(() => getReactions(related, ev.id, EventKind.Deletion), [related]);
+
const { isEventMuted } = useModeration();
const { ref, inView } = useInView({ triggerOnce: true });
+ const { reactions, reposts, deletions, zaps } = useEventReactions(ev, related);
const login = useLogin();
const { pinned, bookmarked } = login;
const publisher = useEventPublisher();
const [translated, setTranslated] = useState();
const { formatMessage } = useIntl();
- const reactions = useMemo(() => getReactions(related, ev.id, EventKind.Reaction), [related, ev]);
- const groupReactions = useMemo(() => {
- const result = reactions?.reduce(
- (acc, reaction) => {
- const kind = normalizeReaction(reaction.content);
- const rs = acc[kind] || [];
- return { ...acc, [kind]: [...rs, reaction] };
- },
- {
- [Reaction.Positive]: [] as TaggedNostrEvent[],
- [Reaction.Negative]: [] as TaggedNostrEvent[],
- },
- );
- return {
- [Reaction.Positive]: dedupeByPubkey(result[Reaction.Positive]),
- [Reaction.Negative]: dedupeByPubkey(result[Reaction.Negative]),
- };
- }, [reactions]);
- const positive = groupReactions[Reaction.Positive];
- const negative = groupReactions[Reaction.Negative];
- const reposts = useMemo(
- () =>
- dedupeByPubkey([
- ...getReactions(related, ev.id, EventKind.TextNote).filter(e => e.tags.some(tagFilterOfTextRepost(e, ev.id))),
- ...getReactions(related, ev.id, EventKind.Repost),
- ]),
- [related, ev],
- );
- const zaps = useMemo(() => {
- const sortedZaps = getReactions(related, ev.id, EventKind.ZapReceipt)
- .map(a => parseZap(a, UserCache, ev))
- .filter(z => z.valid);
- sortedZaps.sort((a, b) => b.amount - a.amount);
- return sortedZaps;
- }, [related]);
- const totalReactions = positive.length + negative.length + reposts.length + zaps.length;
+
+ const totalReactions = reactions.positive.length + reactions.negative.length + reposts.length + zaps.length;
const options = {
showHeader: true,
@@ -117,45 +75,19 @@ export function NoteInner(props: NoteProps) {
}
const innerContent = () => {
- if (ev.kind === EventKind.LongFormTextNote) {
- const title = findTag(ev, "title");
- const summary = findTag(ev, "simmary");
- const image = findTag(ev, "image");
- return (
-
-
{title}
-
-
{summary}
-
- {image &&
}
-
-
- );
- } else {
- const body = ev?.content ?? "";
- return (
-
- );
- }
+ const body = ev?.content ?? "";
+ return (
+
+ );
};
const transformBody = () => {
@@ -278,7 +210,7 @@ export function NoteInner(props: NoteProps) {
);
}
- const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls, EventKind.LongFormTextNote];
+ const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls];
if (!canRenderAsTextNote.includes(ev.kind)) {
const alt = findTag(ev, "alt");
if (alt) {
@@ -374,12 +306,12 @@ export function NoteInner(props: NoteProps) {
)}
- {options.showFooter && }
+ {options.showFooter && }
diff --git a/packages/app/src/Element/Event/Poll.tsx b/packages/app/src/Element/Event/Poll.tsx
index 950dbf46..3223914a 100644
--- a/packages/app/src/Element/Event/Poll.tsx
+++ b/packages/app/src/Element/Event/Poll.tsx
@@ -1,4 +1,4 @@
-import { TaggedNostrEvent, ParsedZap } from "@snort/system";
+import { TaggedNostrEvent, ParsedZap, NostrLink } from "@snort/system";
import { LNURL } from "@snort/shared";
import { useState } from "react";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
@@ -58,7 +58,7 @@ export default function Poll(props: PollProps) {
setVoting(opt);
const r = Object.keys(relays.item);
- const zap = await publisher.zap(amount * 1000, props.ev.pubkey, r, props.ev.id, undefined, eb =>
+ const zap = await publisher.zap(amount * 1000, props.ev.pubkey, r, NostrLink.fromEvent(props.ev), undefined, eb =>
eb.tag(["poll_option", opt.toString()]),
);
diff --git a/packages/app/src/Element/Event/Thread.tsx b/packages/app/src/Element/Event/Thread.tsx
index 20ccb721..78057640 100644
--- a/packages/app/src/Element/Event/Thread.tsx
+++ b/packages/app/src/Element/Event/Thread.tsx
@@ -2,9 +2,9 @@ import "./Thread.css";
import { useMemo, useState, ReactNode, useContext } from "react";
import { useIntl } from "react-intl";
import { useNavigate, useParams } from "react-router-dom";
-import { TaggedNostrEvent, u256, NostrPrefix, EventExt, parseNostrLink } from "@snort/system";
+import { TaggedNostrEvent, u256, NostrPrefix, EventExt, parseNostrLink, NostrLink } from "@snort/system";
-import { getReactions, getAllReactions } from "SnortUtils";
+import { getAllLinkReactions, getLinkReactions } from "SnortUtils";
import BackButton from "Element/BackButton";
import Note from "Element/Event/Note";
import NoteGhost from "Element/Event/NoteGhost";
@@ -248,7 +248,7 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
className={className}
key={note.id}
data={note}
- related={getReactions(thread.reactions, note.id)}
+ related={getLinkReactions(thread.reactions, NostrLink.fromEvent(note))}
options={{ showReactionsLink: true, showMediaSpotlight: !props.disableSpotlight }}
onClick={navigateThread}
/>
@@ -268,9 +268,9 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
a.id),
+ replies.map(a => NostrLink.fromEvent(a)),
)}
chains={thread.chains}
onNavigate={navigateThread}
diff --git a/packages/app/src/Element/HyperText.tsx b/packages/app/src/Element/HyperText.tsx
index 83c6e5a5..a0967536 100644
--- a/packages/app/src/Element/HyperText.tsx
+++ b/packages/app/src/Element/HyperText.tsx
@@ -23,14 +23,16 @@ import WavlakeEmbed from "Element/Embed/WavlakeEmbed";
import LinkPreview from "Element/Embed/LinkPreview";
import NostrLink from "Element/Embed/NostrLink";
import MagnetLink from "Element/Embed/MagnetLink";
+import { ReactNode } from "react";
interface HypeTextProps {
link: string;
+ children?: ReactNode | Array | null;
depth?: number;
showLinkPreview?: boolean;
}
-export default function HyperText({ link, depth, showLinkPreview }: HypeTextProps) {
+export default function HyperText({ link, depth, showLinkPreview, children }: HypeTextProps) {
const a = link;
try {
const url = new URL(a);
@@ -78,7 +80,7 @@ export default function HyperText({ link, depth, showLinkPreview }: HypeTextProp
return (
<>
e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
- {a}
+ {children ?? a}
{/*,*/}
>
@@ -100,7 +102,7 @@ export default function HyperText({ link, depth, showLinkPreview }: HypeTextProp
}
return (
e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
- {a}
+ {children ?? a}
);
}
diff --git a/packages/app/src/Element/Text.css b/packages/app/src/Element/Text.css
index 238a2682..ac6ed74b 100644
--- a/packages/app/src/Element/Text.css
+++ b/packages/app/src/Element/Text.css
@@ -23,53 +23,11 @@
text-decoration: underline;
}
-.text h1 {
- margin: 0;
-}
-.text h2 {
- margin: 0;
-}
-.text h3 {
- margin: 0;
-}
-.text h4 {
- margin: 0;
-}
-.text h5 {
- margin: 0;
-}
-.text h6 {
- margin: 0;
-}
-
-.text p {
- margin: 0;
- margin-bottom: 4px;
-}
-
-.text p:last-child {
- margin-bottom: 0;
-}
-
.text pre {
margin: 0;
overflow: scroll;
}
-.text li {
- margin-top: -1em;
-}
-.text li:last-child {
- margin-bottom: -2em;
-}
-
-.text hr {
- border: 0;
- height: 1px;
- background-image: var(--gray-gradient);
- margin: 20px;
-}
-
.text img:not(.custom-emoji),
.text video,
.text iframe,
@@ -84,13 +42,6 @@
aspect-ratio: 16 / 9;
}
-.text blockquote {
- margin: 0;
- color: var(--font-secondary-color);
- border-left: 2px solid var(--font-secondary-color);
- padding-left: 12px;
-}
-
.gallery {
grid-template-columns: repeat(4, 1fr);
gap: 2px;
diff --git a/packages/app/src/Hooks/useEventReactions.tsx b/packages/app/src/Hooks/useEventReactions.tsx
new file mode 100644
index 00000000..05a22526
--- /dev/null
+++ b/packages/app/src/Hooks/useEventReactions.tsx
@@ -0,0 +1,49 @@
+import { EventKind, NostrLink, parseZap, TaggedNostrEvent } from "@snort/system";
+import { UserCache } from "Cache";
+import { useMemo } from "react";
+import { dedupeByPubkey, getLinkReactions, normalizeReaction, Reaction } from "SnortUtils";
+
+export function useEventReactions(ev: TaggedNostrEvent, related: ReadonlyArray) {
+ return useMemo(() => {
+ const link = NostrLink.fromEvent(ev);
+ const deletions = getLinkReactions(related, link, EventKind.Deletion);
+ const reactions = getLinkReactions(related, link, EventKind.Reaction);
+ const reposts = getLinkReactions(related, link, EventKind.Repost);
+
+ const groupReactions = (() => {
+ const result = reactions?.reduce(
+ (acc, reaction) => {
+ const kind = normalizeReaction(reaction.content);
+ const rs = acc[kind] || [];
+ return { ...acc, [kind]: [...rs, reaction] };
+ },
+ {
+ [Reaction.Positive]: [] as TaggedNostrEvent[],
+ [Reaction.Negative]: [] as TaggedNostrEvent[],
+ },
+ );
+ return {
+ [Reaction.Positive]: dedupeByPubkey(result[Reaction.Positive]),
+ [Reaction.Negative]: dedupeByPubkey(result[Reaction.Negative]),
+ };
+ })();
+ const positive = groupReactions[Reaction.Positive];
+ const negative = groupReactions[Reaction.Negative];
+
+ const zaps = getLinkReactions(related, link, EventKind.ZapReceipt)
+ .map(a => parseZap(a, UserCache, ev))
+ .filter(a => a.valid)
+ .sort((a, b) => b.amount - a.amount);
+
+ return {
+ deletions,
+ reactions: {
+ all: reactions,
+ positive,
+ negative,
+ },
+ reposts,
+ zaps,
+ };
+ }, [ev, related]);
+}
diff --git a/packages/app/src/Login/Preferences.ts b/packages/app/src/Login/Preferences.ts
index f68a22ea..15f09085 100644
--- a/packages/app/src/Login/Preferences.ts
+++ b/packages/app/src/Login/Preferences.ts
@@ -95,7 +95,7 @@ export const DefaultPreferences = {
autoLoadMedia: "all",
theme: "system",
confirmReposts: false,
- showDebugMenus: false,
+ showDebugMenus: true,
autoShowLatest: false,
fileUploader: "void.cat",
imgProxyConfig: DefaultImgProxy,
diff --git a/packages/app/src/Pages/Profile/ProfilePage.tsx b/packages/app/src/Pages/Profile/ProfilePage.tsx
index fb515ddc..01a6615e 100644
--- a/packages/app/src/Pages/Profile/ProfilePage.tsx
+++ b/packages/app/src/Pages/Profile/ProfilePage.tsx
@@ -2,11 +2,19 @@ import "./ProfilePage.css";
import { useEffect, useState } from "react";
import FormattedMessage from "Element/FormattedMessage";
import { useNavigate, useParams } from "react-router-dom";
-import { encodeTLV, encodeTLVEntries, EventKind, NostrPrefix, TLVEntryType, tryParseNostrLink } from "@snort/system";
+import {
+ encodeTLV,
+ encodeTLVEntries,
+ EventKind,
+ NostrLink,
+ NostrPrefix,
+ TLVEntryType,
+ tryParseNostrLink,
+} from "@snort/system";
import { LNURL } from "@snort/shared";
import { useUserProfile } from "@snort/system-react";
-import { findTag, getReactions, unwrap } from "SnortUtils";
+import { findTag, getLinkReactions, unwrap } from "SnortUtils";
import Note from "Element/Event/Note";
import { Tab, TabElement } from "Element/Tabs";
import Icon from "Icons/Icon";
@@ -232,7 +240,7 @@ export default function ProfilePage({ id: propId }: ProfilePageProps) {
);
diff --git a/packages/app/src/SnortUtils/index.ts b/packages/app/src/SnortUtils/index.ts
index 52018793..307cae16 100644
--- a/packages/app/src/SnortUtils/index.ts
+++ b/packages/app/src/SnortUtils/index.ts
@@ -13,6 +13,7 @@ import {
NostrPrefix,
NostrEvent,
MetadataCache,
+ NostrLink,
} from "@snort/system";
export const sha256 = (str: string | Uint8Array): u256 => {
@@ -162,15 +163,20 @@ export function normalizeReaction(content: string) {
}
}
-/**
- * Get reactions to a specific event (#e + kind filter)
- */
-export function getReactions(notes: readonly TaggedNostrEvent[] | undefined, id: u256, kind?: EventKind) {
- return notes?.filter(a => a.kind === (kind ?? a.kind) && a.tags.some(a => a[0] === "e" && a[1] === id)) || [];
+export function getLinkReactions(
+ notes: ReadonlyArray | undefined,
+ link: NostrLink,
+ kind?: EventKind,
+) {
+ return notes?.filter(a => a.kind === (kind ?? a.kind) && link.isReplyToThis(a)) || [];
}
-export function getAllReactions(notes: readonly TaggedNostrEvent[] | undefined, ids: Array, kind?: EventKind) {
- return notes?.filter(a => a.kind === (kind ?? a.kind) && a.tags.some(a => a[0] === "e" && ids.includes(a[1]))) || [];
+export function getAllLinkReactions(
+ notes: readonly TaggedNostrEvent[] | undefined,
+ links: Array,
+ kind?: EventKind,
+) {
+ return notes?.filter(a => a.kind === (kind ?? a.kind) && links.some(b => b.isReplyToThis(a))) || [];
}
export function deepClone(obj: T) {
diff --git a/packages/app/src/Zapper.ts b/packages/app/src/Zapper.ts
index eb211ef8..2950294c 100644
--- a/packages/app/src/Zapper.ts
+++ b/packages/app/src/Zapper.ts
@@ -94,13 +94,7 @@ export class Zapper {
const pub = t.zap?.anon ?? false ? EventPublisher.privateKey(generateRandomKey().privateKey) : this.publisher;
const zap =
t.zap && svc.canZap
- ? await pub?.zap(toSend * 1000, t.zap.pubkey, relays, undefined, t.memo, eb => {
- if (t.zap?.event) {
- const tag = t.zap.event.toEventTag();
- if (tag) {
- eb.tag(tag);
- }
- }
+ ? await pub?.zap(toSend * 1000, t.zap.pubkey, relays, t.zap?.event, t.memo, eb => {
if (t.zap?.anon) {
eb.tag(["anon", ""]);
}
diff --git a/packages/app/src/index.css b/packages/app/src/index.css
index 4b11a8a4..91e1952b 100644
--- a/packages/app/src/index.css
+++ b/packages/app/src/index.css
@@ -709,10 +709,6 @@ div.form-col {
line-height: 36px;
}
-.main-content .profile-preview {
- margin: 8px 0;
-}
-
button.tall {
height: 40px;
}
diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json
index db331317..ff2b39c4 100644
--- a/packages/app/src/lang.json
+++ b/packages/app/src/lang.json
@@ -1353,6 +1353,9 @@
"rx1i0i": {
"defaultMessage": "Short link"
},
+ "s5yJ8G": {
+ "defaultMessage": "Read full story"
+ },
"sKDn4e": {
"defaultMessage": "Show Badges"
},
diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json
index 70111264..1f53403f 100644
--- a/packages/app/src/translations/en.json
+++ b/packages/app/src/translations/en.json
@@ -443,6 +443,7 @@
"rrfdTe": "This is the same technology which is used by Bitcoin and has been proven to be extremely secure.",
"rudscU": "Failed to load follows, please try again later",
"rx1i0i": "Short link",
+ "s5yJ8G": "Read full story",
"sKDn4e": "Show Badges",
"sUNhQE": "user",
"sZQzjQ": "Failed to parse zap split: {input}",
diff --git a/packages/system/src/event-ext.ts b/packages/system/src/event-ext.ts
index 76feff64..739c6979 100644
--- a/packages/system/src/event-ext.ts
+++ b/packages/system/src/event-ext.ts
@@ -105,7 +105,6 @@ export abstract class EventExt {
}
static extractThread(ev: NostrEvent) {
- const shouldWriteMarkers = ev.kind === EventKind.TextNote;
const ret = {
mentions: [],
pubKeys: [],
@@ -115,16 +114,14 @@ export abstract class EventExt {
const marked = replyTags.some(a => a.marker);
if (!marked) {
ret.root = replyTags[0];
- ret.root.marker = shouldWriteMarkers ? "root" : undefined;
+ ret.root.marker = "root";
if (replyTags.length > 1) {
ret.replyTo = replyTags[replyTags.length - 1];
- ret.replyTo.marker = shouldWriteMarkers ? "reply" : undefined;
+ ret.replyTo.marker = "reply";
}
if (replyTags.length > 2) {
ret.mentions = replyTags.slice(1, -1);
- if (shouldWriteMarkers) {
- ret.mentions.forEach(a => (a.marker = "mention"));
- }
+ ret.mentions.forEach(a => (a.marker = "mention"));
}
} else {
const root = replyTags.find(a => a.marker === "root");
diff --git a/packages/system/src/event-publisher.ts b/packages/system/src/event-publisher.ts
index b2bd0ad4..df5c17a9 100644
--- a/packages/system/src/event-publisher.ts
+++ b/packages/system/src/event-publisher.ts
@@ -165,14 +165,14 @@ export class EventPublisher {
amount: number,
author: HexKey,
relays: Array,
- note?: HexKey,
+ note?: NostrLink,
msg?: string,
fnExtra?: EventBuilderHook,
) {
const eb = this.#eb(EventKind.ZapRequest);
eb.content(msg ?? "");
if (note) {
- eb.tag(["e", note]);
+ eb.tag(unwrap(note.toEventTag()));
}
eb.tag(["p", author]);
eb.tag(["relays", ...relays.map(a => a.trim())]);
@@ -205,7 +205,7 @@ export class EventPublisher {
eb.tag(["p", pk]);
}
} else {
- eb.tag([...(NostrLink.fromEvent(replyTo).toEventTag() ?? []), "reply"]);
+ eb.tag([...(NostrLink.fromEvent(replyTo).toEventTag() ?? []), "root"]);
// dont tag self in replies
if (replyTo.pubkey !== this.#pubKey) {
eb.tag(["p", replyTo.pubkey]);
@@ -219,7 +219,7 @@ export class EventPublisher {
async react(evRef: NostrEvent, content = "+") {
const eb = this.#eb(EventKind.Reaction);
eb.content(content);
- eb.tag(["e", evRef.id]);
+ eb.tag(unwrap(NostrLink.fromEvent(evRef).toEventTag()));
eb.tag(["p", evRef.pubkey]);
return await this.#sign(eb);
}
@@ -269,7 +269,7 @@ export class EventPublisher {
*/
async repost(note: NostrEvent) {
const eb = this.#eb(EventKind.Repost);
- eb.tag(["e", note.id, ""]);
+ eb.tag(unwrap(NostrLink.fromEvent(note).toEventTag()));
eb.tag(["p", note.pubkey]);
return await this.#sign(eb);
}
diff --git a/packages/system/src/nostr-link.ts b/packages/system/src/nostr-link.ts
index 42e56868..98a20b16 100644
--- a/packages/system/src/nostr-link.ts
+++ b/packages/system/src/nostr-link.ts
@@ -1,5 +1,5 @@
import { bech32ToHex, hexToBech32, unwrap } from "@snort/shared";
-import { NostrPrefix, decodeTLV, TLVEntryType, encodeTLV, NostrEvent, TaggedNostrEvent } from ".";
+import { NostrPrefix, decodeTLV, TLVEntryType, encodeTLV, NostrEvent, TaggedNostrEvent, EventExt, Tag } from ".";
import { findTag } from "./utils";
export class NostrLink {
@@ -43,6 +43,79 @@ export class NostrLink {
return false;
}
+ /**
+ * Is the supplied event a reply to this link
+ */
+ isReplyToThis(ev: NostrEvent) {
+ const thread = EventExt.extractThread(ev);
+ if (!thread) return false; // non-thread events are not replies
+
+ if (!thread.root) return false; // must have root marker or positional e/a tag in position 0
+
+ if (
+ thread.root.key === "e" &&
+ thread.root.value === this.id &&
+ (this.type === NostrPrefix.Event || this.type === NostrPrefix.Note)
+ ) {
+ return true;
+ }
+ if (thread.root.key === "a" && this.type === NostrPrefix.Address) {
+ const [kind, author, dTag] = unwrap(thread.root.value).split(":");
+ if (Number(kind) === this.kind && author === this.author && dTag === this.id) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Does the supplied event contain a tag matching this link
+ */
+ referencesThis(ev: NostrEvent) {
+ for (const t of ev.tags) {
+ if (t[0] === "e" && t[1] === this.id && (this.type === NostrPrefix.Event || this.type === NostrPrefix.Note)) {
+ return true;
+ }
+ if (t[0] === "a" && this.type === NostrPrefix.Address) {
+ const [kind, author, dTag] = t[1].split(":");
+ if (Number(kind) === this.kind && author === this.author && dTag === this.id) {
+ return true;
+ }
+ }
+ if (
+ t[0] === "p" &&
+ (this.type === NostrPrefix.Profile || this.type === NostrPrefix.PublicKey) &&
+ this.id === t[1]
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ equals(other: NostrLink) {
+ if (other.type === this.type && this.type === NostrPrefix.Address) {
+ }
+ }
+
+ static fromThreadTag(tag: Tag) {
+ const relay = tag.relay ? [tag.relay] : undefined;
+
+ switch (tag.key) {
+ case "e": {
+ return new NostrLink(NostrPrefix.Event, unwrap(tag.value), undefined, undefined, relay);
+ }
+ case "p": {
+ return new NostrLink(NostrPrefix.Profile, unwrap(tag.value), undefined, undefined, relay);
+ }
+ case "a": {
+ const [kind, author, dTag] = unwrap(tag.value).split(":");
+ return new NostrLink(NostrPrefix.Address, dTag, Number(kind), author, relay);
+ }
+ }
+ throw new Error(`Unknown tag kind ${tag.key}`);
+ }
+
static fromTag(tag: Array) {
const relays = tag.length > 2 ? [tag[2]] : undefined;
switch (tag[0]) {
diff --git a/yarn.lock b/yarn.lock
index 5f2ef34f..8ad91a97 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2734,6 +2734,8 @@ __metadata:
jest: ^29.5.0
jest-environment-jsdom: ^29.5.0
light-bolt11-decoder: ^2.1.0
+ marked: ^9.1.0
+ marked-footnote: ^1.0.0
match-sorter: ^6.3.1
mini-css-extract-plugin: ^2.7.5
prettier: 2.8.3
@@ -9480,6 +9482,24 @@ __metadata:
languageName: node
linkType: hard
+"marked-footnote@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "marked-footnote@npm:1.0.0"
+ peerDependencies:
+ marked: ">=7.0.0"
+ checksum: 14f11592bf936ca32d1a43d55ef0df92e5319e8d3e9df517b5f41ed50d76e2b38f782ad475bd16933f026629c357da4c95e46aa0edf4cb196a8c475086fc2909
+ languageName: node
+ linkType: hard
+
+"marked@npm:^9.1.0":
+ version: 9.1.0
+ resolution: "marked@npm:9.1.0"
+ bin:
+ marked: bin/marked.js
+ checksum: 452a5f564719c93a55136d77e6aa51852df9b24a4359c74d6b2c661bbb09fc8db1bb5ee0b9a8c0eb6d0ba22ec4a3af110bc97ba881e4ffae9f5e83c3ce2676d2
+ languageName: node
+ linkType: hard
+
"match-sorter@npm:^6.3.1":
version: 6.3.1
resolution: "match-sorter@npm:6.3.1"