From b765cb29b7cb31816e62328c020384192aabe2f7 Mon Sep 17 00:00:00 2001 From: Kieran Date: Fri, 10 Nov 2023 13:17:19 +0000 Subject: [PATCH] refactor: update nip51 support --- packages/app/src/Element/Event/Note.tsx | 2 +- .../app/src/Element/Event/NoteContextMenu.tsx | 11 +-- packages/app/src/Element/Event/NoteInner.tsx | 6 +- packages/app/src/Feed/BadgesFeed.ts | 4 +- packages/app/src/Feed/BookmarkFeed.tsx | 9 --- packages/app/src/Feed/LoginFeed.ts | 68 +++++++++---------- packages/app/src/Feed/MuteList.ts | 52 -------------- packages/app/src/Feed/PinnedFeed.tsx | 8 --- packages/app/src/Hooks/useLists.tsx | 64 +++++++++++++++++ .../app/src/Hooks/useNotelistSubscription.ts | 44 ------------ packages/app/src/Pages/HashTagsPage.tsx | 6 +- packages/app/src/Pages/ListFeedPage.tsx | 2 +- .../app/src/Pages/Profile/ProfilePage.tsx | 13 ++-- packages/app/src/Pages/Profile/ProfileTab.tsx | 4 +- packages/app/src/Pages/onboarding/topics.tsx | 7 +- packages/system/src/event-kind.ts | 12 +++- packages/system/src/event-publisher.ts | 42 +++++++----- packages/system/src/nostr-link.ts | 32 +++++++-- packages/system/src/nostr.ts | 11 --- 19 files changed, 192 insertions(+), 205 deletions(-) delete mode 100644 packages/app/src/Feed/BookmarkFeed.tsx delete mode 100644 packages/app/src/Feed/MuteList.ts delete mode 100644 packages/app/src/Feed/PinnedFeed.tsx create mode 100644 packages/app/src/Hooks/useLists.tsx delete mode 100644 packages/app/src/Hooks/useNotelistSubscription.ts diff --git a/packages/app/src/Element/Event/Note.tsx b/packages/app/src/Element/Event/Note.tsx index 21b5c855..0a07967d 100644 --- a/packages/app/src/Element/Event/Note.tsx +++ b/packages/app/src/Element/Event/Note.tsx @@ -49,7 +49,7 @@ export default function Note(props: NoteProps) { if (ev.kind === EventKind.ZapstrTrack) { return ; } - if (ev.kind === EventKind.PubkeyLists || ev.kind === EventKind.ContactList) { + if (ev.kind === EventKind.CategorizedPeople || ev.kind === EventKind.ContactList) { return ; } if (ev.kind === EventKind.LiveEvent) { diff --git a/packages/app/src/Element/Event/NoteContextMenu.tsx b/packages/app/src/Element/Event/NoteContextMenu.tsx index 7ff1b967..f1794070 100644 --- a/packages/app/src/Element/Event/NoteContextMenu.tsx +++ b/packages/app/src/Element/Event/NoteContextMenu.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { HexKey, Lists, NostrLink, TaggedNostrEvent } from "@snort/system"; +import { HexKey, NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system"; import { Menu, MenuItem } from "@szhsin/react-menu"; import Icon from "Icons/Icon"; @@ -96,16 +96,19 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) { async function pin(id: HexKey) { if (publisher) { const es = [...login.pinned.item, id]; - const ev = await publisher.noteList(es, Lists.Pinned); + const ev = await publisher.pinned(es.map(a => new NostrLink(NostrPrefix.Note, a))); system.BroadcastEvent(ev); setPinned(login, es, ev.created_at * 1000); } } - async function bookmark(id: HexKey) { + async function bookmark(id: string) { if (publisher) { const es = [...login.bookmarked.item, id]; - const ev = await publisher.noteList(es, Lists.Bookmarked); + const ev = await publisher.bookmarks( + es.map(a => new NostrLink(NostrPrefix.Note, a)), + "bookmark", + ); system.BroadcastEvent(ev); setBookmarked(login, es, ev.created_at * 1000); } diff --git a/packages/app/src/Element/Event/NoteInner.tsx b/packages/app/src/Element/Event/NoteInner.tsx index f7a5bc64..bb6c42e3 100644 --- a/packages/app/src/Element/Event/NoteInner.tsx +++ b/packages/app/src/Element/Event/NoteInner.tsx @@ -3,7 +3,7 @@ import React, { ReactNode, useMemo, useState } from "react"; import { useInView } from "react-intersection-observer"; import { FormattedMessage, useIntl } from "react-intl"; import classNames from "classnames"; -import { EventExt, EventKind, HexKey, Lists, NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system"; +import { EventExt, EventKind, HexKey, NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system"; import { useEventReactions } from "@snort/system-react"; import { findTag, hexToBech32 } from "SnortUtils"; @@ -60,7 +60,7 @@ export function NoteInner(props: NoteProps) { if (options.canUnpin && publisher) { if (window.confirm(formatMessage(messages.ConfirmUnpin))) { const es = pinned.item.filter(e => e !== id); - const ev = await publisher.noteList(es, Lists.Pinned); + const ev = await publisher.pinned(es.map(a => new NostrLink(NostrPrefix.Note, a))); system.BroadcastEvent(ev); setPinned(login, es, ev.created_at * 1000); } @@ -71,7 +71,7 @@ export function NoteInner(props: NoteProps) { if (options.canUnbookmark && publisher) { if (window.confirm(formatMessage(messages.ConfirmUnbookmark))) { const es = bookmarked.item.filter(e => e !== id); - const ev = await publisher.noteList(es, Lists.Bookmarked); + const ev = await publisher.pinned(es.map(a => new NostrLink(NostrPrefix.Note, a))); system.BroadcastEvent(ev); setBookmarked(login, es, ev.created_at * 1000); } diff --git a/packages/app/src/Feed/BadgesFeed.ts b/packages/app/src/Feed/BadgesFeed.ts index d180f160..d74d2ee8 100644 --- a/packages/app/src/Feed/BadgesFeed.ts +++ b/packages/app/src/Feed/BadgesFeed.ts @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { EventKind, HexKey, Lists, RequestBuilder, ReplaceableNoteStore, NoteCollection } from "@snort/system"; +import { EventKind, HexKey, RequestBuilder, ReplaceableNoteStore, NoteCollection } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import { unwrap, findTag, chunks } from "SnortUtils"; @@ -13,7 +13,7 @@ export default function useProfileBadges(pubkey?: HexKey) { const sub = useMemo(() => { if (!pubkey) return null; const b = new RequestBuilder(`badges:${pubkey.slice(0, 12)}`); - b.withFilter().kinds([EventKind.ProfileBadges]).tag("d", [Lists.Badges]).authors([pubkey]); + b.withFilter().kinds([EventKind.ProfileBadges]).tag("d", ["profile_badges"]).authors([pubkey]); return b; }, [pubkey]); diff --git a/packages/app/src/Feed/BookmarkFeed.tsx b/packages/app/src/Feed/BookmarkFeed.tsx deleted file mode 100644 index 2c7da28e..00000000 --- a/packages/app/src/Feed/BookmarkFeed.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { HexKey, Lists } from "@snort/system"; - -import useNotelistSubscription from "Hooks/useNotelistSubscription"; -import useLogin from "Hooks/useLogin"; - -export default function useBookmarkFeed(pubkey?: HexKey) { - const { bookmarked } = useLogin(); - return useNotelistSubscription(pubkey, Lists.Bookmarked, bookmarked.item); -} diff --git a/packages/app/src/Feed/LoginFeed.ts b/packages/app/src/Feed/LoginFeed.ts index e2322f33..f7eb16b1 100644 --- a/packages/app/src/Feed/LoginFeed.ts +++ b/packages/app/src/Feed/LoginFeed.ts @@ -1,11 +1,10 @@ import { useEffect, useMemo } from "react"; -import { TaggedNostrEvent, Lists, EventKind, RequestBuilder, NoteCollection } from "@snort/system"; +import { TaggedNostrEvent, EventKind, RequestBuilder, NoteCollection, NostrLink } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; -import { bech32ToHex, getNewest, getNewestEventTagsByKey, unwrap } from "SnortUtils"; +import { bech32ToHex, findTag, getNewest, getNewestEventTagsByKey, unwrap } from "SnortUtils"; import { makeNotification, sendNotification } from "Notifications"; import useEventPublisher from "Hooks/useEventPublisher"; -import { getMutedKeys } from "Feed/MuteList"; import useModeration from "Hooks/useModeration"; import useLogin from "Hooks/useLogin"; import { @@ -51,7 +50,10 @@ export default function useLoginFeed() { b.withOptions({ leaveOpen: true, }); - b.withFilter().authors([pubKey]).kinds([EventKind.ContactList, EventKind.Relays]); + b.withFilter() + .authors([pubKey]) + .kinds([EventKind.ContactList, EventKind.Relays, EventKind.MuteList, EventKind.PinList]); + b.withFilter().authors([pubKey]).kinds([EventKind.CategorizedBookmarks]).tag("d", ["follow", "bookmark"]); if (CONFIG.features.subscriptions && !login.readonly) { b.withFilter().authors([pubKey]).kinds([EventKind.AppData]).tag("d", ["snort"]); b.withFilter() @@ -61,10 +63,6 @@ export default function useLoginFeed() { .tag("p", [pubKey]) .limit(10); } - b.withFilter() - .authors([pubKey]) - .kinds([EventKind.PubkeyLists]) - .tag("d", [Lists.Muted, Lists.Followed, Lists.Pinned, Lists.Bookmarked]); const n4Sub = Nip4Chats.subscription(login); if (n4Sub) { @@ -151,23 +149,26 @@ export default function useLoginFeed() { } }, [loginFeed, readNotifications]); - function handleMutedFeed(mutedFeed: TaggedNostrEvent[]) { - const muted = getMutedKeys(mutedFeed); - setMuted(login, muted.keys, muted.createdAt * 1000); + async function handleMutedFeed(mutedFeed: TaggedNostrEvent[]) { + const latest = getNewest(mutedFeed); + if (!latest) return; - if (muted.raw && (muted.raw?.content?.length ?? 0) > 0 && pubKey) { - publisher - ?.nip4Decrypt(muted.raw.content, pubKey) - .then(plaintext => { - try { - const blocked = JSON.parse(plaintext); - const keys = blocked.filter((p: string) => p && p.length === 2 && p[0] === "p").map((p: string) => p[1]); - setBlocked(login, keys, unwrap(muted.raw).created_at * 1000); - } catch (error) { - console.debug("Couldn't parse JSON"); - } - }) - .catch(error => console.warn(error)); + const muted = NostrLink.fromTags(latest.tags); + setMuted( + login, + muted.map(a => a.id), + latest.created_at * 1000, + ); + + if (latest?.content && publisher && pubKey) { + try { + const privMutes = await publisher.nip4Decrypt(latest.content, pubKey); + const blocked = JSON.parse(privMutes) as Array>; + const keys = blocked.filter(a => a[0] === "p").map(a => a[1]); + setBlocked(login, keys, latest.created_at * 1000); + } catch (error) { + console.debug("Failed to parse mute list", error, latest); + } } } @@ -194,23 +195,20 @@ export default function useLoginFeed() { useEffect(() => { if (loginFeed.data) { - const getList = (evs: readonly TaggedNostrEvent[], list: Lists) => - evs - .filter( - a => a.kind === EventKind.TagLists || a.kind === EventKind.NoteLists || a.kind === EventKind.PubkeyLists, - ) - .filter(a => unwrap(a.tags.find(b => b[0] === "d"))[1] === list); - - const mutedFeed = getList(loginFeed.data, Lists.Muted); + const mutedFeed = loginFeed.data.filter(a => a.kind === EventKind.MuteList); handleMutedFeed(mutedFeed); - const pinnedFeed = getList(loginFeed.data, Lists.Pinned); + const pinnedFeed = loginFeed.data.filter(a => a.kind === EventKind.PinList); handlePinnedFeed(pinnedFeed); - const tagsFeed = getList(loginFeed.data, Lists.Followed); + const tagsFeed = loginFeed.data.filter( + a => a.kind === EventKind.CategorizedBookmarks && findTag(a, "d") === "follow", + ); handleTagFeed(tagsFeed); - const bookmarkFeed = getList(loginFeed.data, Lists.Bookmarked); + const bookmarkFeed = loginFeed.data.filter( + a => a.kind === EventKind.CategorizedBookmarks && findTag(a, "d") === "bookmark", + ); handleBookmarkFeed(bookmarkFeed); } }, [loginFeed]); diff --git a/packages/app/src/Feed/MuteList.ts b/packages/app/src/Feed/MuteList.ts deleted file mode 100644 index 6abf9bcd..00000000 --- a/packages/app/src/Feed/MuteList.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useMemo } from "react"; -import { HexKey, TaggedNostrEvent, Lists, EventKind, NoteCollection, RequestBuilder } from "@snort/system"; -import { useRequestBuilder } from "@snort/system-react"; - -import { getNewest } from "SnortUtils"; -import useLogin from "Hooks/useLogin"; - -export default function useMutedFeed(pubkey?: HexKey) { - const { publicKey, muted } = useLogin(); - const isMe = publicKey === pubkey; - - const sub = useMemo(() => { - if (isMe || !pubkey) return null; - const b = new RequestBuilder(`muted:${pubkey.slice(0, 12)}`); - b.withFilter().authors([pubkey]).kinds([EventKind.PubkeyLists]).tag("d", [Lists.Muted]); - return b; - }, [pubkey]); - - const mutedFeed = useRequestBuilder(NoteCollection, sub); - - const mutedList = useMemo(() => { - if (pubkey && mutedFeed.data) { - return getMuted(mutedFeed.data, pubkey); - } - return []; - }, [mutedFeed, pubkey]); - - return isMe ? muted.item : mutedList; -} - -export function getMutedKeys(rawNotes: TaggedNostrEvent[]): { - createdAt: number; - keys: HexKey[]; - raw?: TaggedNostrEvent; -} { - const newest = getNewest(rawNotes); - if (newest) { - const { created_at, tags } = newest; - const keys = tags.filter(t => t[0] === "p").map(t => t[1]); - return { - raw: newest, - keys, - createdAt: created_at, - }; - } - return { createdAt: 0, keys: [] }; -} - -export function getMuted(feed: readonly TaggedNostrEvent[], pubkey: HexKey): HexKey[] { - const lists = feed.filter(a => a.kind === EventKind.PubkeyLists && a.pubkey === pubkey); - return getMutedKeys(lists).keys; -} diff --git a/packages/app/src/Feed/PinnedFeed.tsx b/packages/app/src/Feed/PinnedFeed.tsx deleted file mode 100644 index bd714903..00000000 --- a/packages/app/src/Feed/PinnedFeed.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { HexKey, Lists } from "@snort/system"; -import useNotelistSubscription from "Hooks/useNotelistSubscription"; -import useLogin from "Hooks/useLogin"; - -export default function usePinnedFeed(pubkey?: HexKey) { - const pinned = useLogin().pinned.item; - return useNotelistSubscription(pubkey, Lists.Pinned, pinned); -} diff --git a/packages/app/src/Hooks/useLists.tsx b/packages/app/src/Hooks/useLists.tsx new file mode 100644 index 00000000..01f0c4b8 --- /dev/null +++ b/packages/app/src/Hooks/useLists.tsx @@ -0,0 +1,64 @@ +import { removeUndefined } from "@snort/shared"; +import { EventKind, NostrLink, NoteCollection, RequestBuilder } from "@snort/system"; +import { useEventsFeed, useRequestBuilder } from "@snort/system-react"; +import { useMemo } from "react"; + +/** + * Use a link event containing e/a/p/t tags + */ +export function useLinkList(id: string, fn: (rb: RequestBuilder) => void) { + const sub = useMemo(() => { + const rb = new RequestBuilder(id); + fn(rb); + return rb; + }, [id, fn]); + + const listStore = useRequestBuilder(NoteCollection, sub); + return useMemo(() => { + if (listStore.data && listStore.data.length > 0) { + return removeUndefined( + listStore.data + .map(e => + e.tags.map(a => { + try { + return NostrLink.fromTag(a); + } catch { + // ignored, skipped + } + }), + ) + .flat(), + ); + } + return []; + }, [listStore.data]); +} + +export function useLinkListEvents(id: string, fn: (rb: RequestBuilder) => void) { + const links = useLinkList(id, fn); + return useEventsFeed(`${id}:events`, links).data ?? []; +} + +export function usePinList(pubkey: string | undefined) { + return useLinkListEvents(`pins:${pubkey?.slice(0, 12)}`, rb => { + if (pubkey) { + rb.withFilter().kinds([EventKind.PinList]).authors([pubkey]); + } + }); +} + +export function useMuteList(pubkey: string | undefined) { + return useLinkList(`pins:${pubkey?.slice(0, 12)}`, rb => { + if (pubkey) { + rb.withFilter().kinds([EventKind.MuteList]).authors([pubkey]); + } + }); +} + +export default function useCategorizedBookmarks(pubkey: string | undefined, list: string) { + return useLinkListEvents(`categorized-bookmarks:${list}:${pubkey?.slice(0, 12)}`, rb => { + if (pubkey) { + rb.withFilter().kinds([EventKind.CategorizedBookmarks]).authors([pubkey]).tag("d", [list]); + } + }); +} diff --git a/packages/app/src/Hooks/useNotelistSubscription.ts b/packages/app/src/Hooks/useNotelistSubscription.ts deleted file mode 100644 index 83002d1f..00000000 --- a/packages/app/src/Hooks/useNotelistSubscription.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useMemo } from "react"; -import { HexKey, Lists, EventKind, FlatNoteStore, NoteCollection, RequestBuilder } from "@snort/system"; -import { useRequestBuilder } from "@snort/system-react"; - -import useLogin from "Hooks/useLogin"; - -export default function useNotelistSubscription(pubkey: HexKey | undefined, l: Lists, defaultIds: HexKey[]) { - const { preferences, publicKey } = useLogin(); - const isMe = publicKey === pubkey; - - const sub = useMemo(() => { - if (isMe || !pubkey) return null; - const rb = new RequestBuilder(`note-list-${l}:${pubkey.slice(0, 12)}`); - rb.withFilter().kinds([EventKind.NoteLists]).authors([pubkey]).tag("d", [l]).limit(1); - - return rb; - }, [pubkey]); - - const listStore = useRequestBuilder(NoteCollection, sub); - const etags = useMemo(() => { - if (isMe) return defaultIds; - // there should only be a single event here because we only load 1 pubkey - if (listStore.data && listStore.data.length > 0) { - return listStore.data[0].tags.filter(a => a[0] === "e").map(a => a[1]); - } - return []; - }, [listStore.data, isMe, defaultIds]); - - const esub = useMemo(() => { - if (!pubkey || etags.length === 0) return null; - const s = new RequestBuilder(`${l}-notes:${pubkey.slice(0, 12)}`); - s.withFilter().kinds([EventKind.TextNote]).ids(etags); - if (etags.length > 0 && preferences.enableReactions) { - s.withFilter() - .kinds([EventKind.Reaction, EventKind.Repost, EventKind.Deletion, EventKind.ZapReceipt]) - .tag("e", etags); - } - return s; - }, [etags, pubkey, preferences]); - - const store = useRequestBuilder(FlatNoteStore, esub); - - return store.data ?? []; -} diff --git a/packages/app/src/Pages/HashTagsPage.tsx b/packages/app/src/Pages/HashTagsPage.tsx index e4647394..40e41ac8 100644 --- a/packages/app/src/Pages/HashTagsPage.tsx +++ b/packages/app/src/Pages/HashTagsPage.tsx @@ -1,6 +1,7 @@ import { useMemo } from "react"; import { useParams } from "react-router-dom"; import { FormattedMessage } from "react-intl"; +import { NostrHashtagLink } from "@snort/system"; import Timeline from "Element/Feed/Timeline"; import useEventPublisher from "Hooks/useEventPublisher"; @@ -18,7 +19,10 @@ const HashTagsPage = () => { async function followTags(ts: string[]) { if (publisher) { - const ev = await publisher.tags(ts); + const ev = await publisher.bookmarks( + ts.map(a => new NostrHashtagLink(a)), + "follow", + ); system.BroadcastEvent(ev); setTags(login, ts, ev.created_at * 1000); } diff --git a/packages/app/src/Pages/ListFeedPage.tsx b/packages/app/src/Pages/ListFeedPage.tsx index fe9a329e..763c0394 100644 --- a/packages/app/src/Pages/ListFeedPage.tsx +++ b/packages/app/src/Pages/ListFeedPage.tsx @@ -14,7 +14,7 @@ export function ListFeedPage() { const { data } = useEventFeed(link); if (!data) return ; - if (data.kind !== EventKind.ContactList && data.kind !== EventKind.PubkeyLists) { + if (data.kind !== EventKind.ContactList && data.kind !== EventKind.CategorizedPeople) { return ( diff --git a/packages/app/src/Pages/Profile/ProfilePage.tsx b/packages/app/src/Pages/Profile/ProfilePage.tsx index f8c99a47..8c427ef7 100644 --- a/packages/app/src/Pages/Profile/ProfilePage.tsx +++ b/packages/app/src/Pages/Profile/ProfilePage.tsx @@ -19,8 +19,6 @@ import { findTag, getLinkReactions, unwrap } from "SnortUtils"; import Note from "Element/Event/Note"; import { Tab, TabElement } from "Element/Tabs"; import Icon from "Icons/Icon"; -import useMutedFeed from "Feed/MuteList"; -import usePinnedFeed from "Feed/PinnedFeed"; import useFollowsFeed from "Feed/FollowsFeed"; import useProfileBadges from "Feed/BadgesFeed"; import useModeration from "Hooks/useModeration"; @@ -47,8 +45,6 @@ import { EmailRegex } from "Const"; import useLogin from "Hooks/useLogin"; import { ZapTarget } from "Zapper"; import { useStatusFeed } from "Feed/StatusFeed"; - -import messages from "../messages"; import { SpotlightMediaModal } from "Element/SpotlightMedia"; import ProfileTab, { BookMarksTab, @@ -60,6 +56,9 @@ import ProfileTab, { } from "Pages/Profile/ProfileTab"; import DisplayName from "Element/User/DisplayName"; import { UserWebsiteLink } from "Element/User/UserWebsiteLink"; +import { useMuteList, usePinList } from "Hooks/useLists"; + +import messages from "../messages"; interface ProfilePageProps { id?: string; @@ -95,8 +94,8 @@ export default function ProfilePage({ id: propId, state }: ProfilePageProps) { // feeds const { blocked } = useModeration(); - const pinned = usePinnedFeed(id); - const muted = useMutedFeed(id); + const pinned = usePinList(id); + const muted = useMuteList(id); const badges = useProfileBadges(showBadges ? id : undefined); const follows = useFollowsFeed(id); const status = useStatusFeed(showStatus ? id : undefined, true); @@ -273,7 +272,7 @@ export default function ProfilePage({ id: propId, state }: ProfilePageProps) { return ; } case ProfileTabType.MUTED: { - return ; + return a.id)} />; } case ProfileTabType.BLOCKED: { return ; diff --git a/packages/app/src/Pages/Profile/ProfileTab.tsx b/packages/app/src/Pages/Profile/ProfileTab.tsx index b000433c..e4bbe1e5 100644 --- a/packages/app/src/Pages/Profile/ProfileTab.tsx +++ b/packages/app/src/Pages/Profile/ProfileTab.tsx @@ -8,11 +8,11 @@ import FollowsList from "Element/User/FollowListBase"; import useFollowsFeed from "Feed/FollowsFeed"; import useRelaysFeed from "Feed/RelaysFeed"; import RelaysMetadata from "Element/Relay/RelaysMetadata"; -import useBookmarkFeed from "Feed/BookmarkFeed"; import Bookmarks from "Element/User/Bookmarks"; import Icon from "Icons/Icon"; import { Tab } from "Element/Tabs"; import { default as ZapElement } from "Element/Event/Zap"; +import useCategorizedBookmarks from "Hooks/useLists"; import messages from "../messages"; @@ -59,7 +59,7 @@ export function RelaysTab({ id }: { id: HexKey }) { } export function BookMarksTab({ id }: { id: HexKey }) { - const bookmarks = useBookmarkFeed(id); + const bookmarks = useCategorizedBookmarks(id, "bookmark"); return ( topics.includes(k)) .map(([, v]) => v.tags) - .flat(); + .flat() + .map(a => new NostrHashtagLink(a)); + if (tags.length > 0) { - const ev = await publisher?.tags(tags); + const ev = await publisher?.bookmarks(tags, "follow"); if (ev) { await system.BroadcastEvent(ev); } diff --git a/packages/system/src/event-kind.ts b/packages/system/src/event-kind.ts index e144a7b5..b286eda1 100644 --- a/packages/system/src/event-kind.ts +++ b/packages/system/src/event-kind.ts @@ -24,11 +24,17 @@ enum EventKind { Relays = 10002, // NIP-65 Ephemeral = 20_000, Auth = 22242, // NIP-42 - PubkeyLists = 30000, // NIP-51a - NoteLists = 30001, // NIP-51b + + MuteList = 10_000, // NIP-51 + PinList = 10_001, // NIP-51 + + CategorizedPeople = 30000, // NIP-51a + CategorizedBookmarks = 30001, // NIP-51b + TagLists = 30002, // NIP-51c Badge = 30009, // NIP-58 ProfileBadges = 30008, // NIP-58 + LongFormTextNote = 30023, // NIP-23 AppData = 30_078, // NIP-78 LiveEvent = 30311, // NIP-102 @@ -37,7 +43,7 @@ enum EventKind { SimpleChatMetadata = 39_000, // NIP-29 ZapRequest = 9734, // NIP 57 ZapReceipt = 9735, // NIP 57 - HttpAuthentication = 27235, // NIP XX - HTTP Authentication + HttpAuthentication = 27235, // NIP 98 - HTTP Authentication } export default EventKind; diff --git a/packages/system/src/event-publisher.ts b/packages/system/src/event-publisher.ts index 09a1ad26..df1092b4 100644 --- a/packages/system/src/event-publisher.ts +++ b/packages/system/src/event-publisher.ts @@ -1,6 +1,6 @@ import * as secp from "@noble/curves/secp256k1"; import * as utils from "@noble/curves/abstract/utils"; -import { unwrap, getPublicKey, unixNow } from "@snort/shared"; +import { unwrap } from "@snort/shared"; import { decodeEncryptionPayload, @@ -8,7 +8,6 @@ import { EventSigner, FullRelaySettings, HexKey, - Lists, MessageEncryptorVersion, NostrEvent, NostrLink, @@ -18,6 +17,7 @@ import { RelaySettings, SignerSupports, TaggedNostrEvent, + ToNostrEventTag, u256, UserMetadata, } from "."; @@ -104,11 +104,14 @@ export class EventPublisher { return await this.#sign(eb); } - async muted(keys: HexKey[], priv: HexKey[]) { - const eb = this.#eb(EventKind.PubkeyLists); - - eb.tag(["d", Lists.Muted]); - keys.forEach(p => { + /** + * Build a mute list event using lists of pubkeys + * @param pub Public mute list + * @param priv Private mute list + */ + async muted(pub: Array, priv: Array) { + const eb = this.#eb(EventKind.MuteList); + pub.forEach(p => { eb.tag(["p", p]); }); if (priv.length > 0) { @@ -119,20 +122,26 @@ export class EventPublisher { return await this.#sign(eb); } - async noteList(notes: u256[], list: Lists) { - const eb = this.#eb(EventKind.NoteLists); - eb.tag(["d", list]); + /** + * Build a pin list event using lists of event links + */ + async pinned(notes: Array) { + const eb = this.#eb(EventKind.PinList); notes.forEach(n => { - eb.tag(["e", n]); + eb.tag(unwrap(n.toEventTag())); }); return await this.#sign(eb); } - async tags(tags: string[]) { - const eb = this.#eb(EventKind.TagLists); - eb.tag(["d", Lists.Followed]); - tags.forEach(t => { - eb.tag(["t", t]); + /** + * Build a categorized bookmarks event with a given label + * @param notes List of bookmarked links + */ + async bookmarks(notes: Array, list: "bookmark" | "follow") { + const eb = this.#eb(EventKind.CategorizedBookmarks); + eb.tag(["d", list]); + notes.forEach(n => { + eb.tag(unwrap(n.toEventTag())); }); return await this.#sign(eb); } @@ -263,6 +272,7 @@ export class EventPublisher { eb.tag(["e", id]); return await this.#sign(eb); } + /** * Repost a note (NIP-18) */ diff --git a/packages/system/src/nostr-link.ts b/packages/system/src/nostr-link.ts index 697be955..353fe45b 100644 --- a/packages/system/src/nostr-link.ts +++ b/packages/system/src/nostr-link.ts @@ -1,4 +1,4 @@ -import { bech32ToHex, hexToBech32, isHex, unwrap } from "@snort/shared"; +import { bech32ToHex, hexToBech32, isHex, removeUndefined, unwrap } from "@snort/shared"; import { decodeTLV, encodeTLV, @@ -12,7 +12,19 @@ import { } from "."; import { findTag } from "./utils"; -export class NostrLink { +export interface ToNostrEventTag { + toEventTag(): Array | undefined; +} + +export class NostrHashtagLink implements ToNostrEventTag { + constructor(readonly tag: string) {} + + toEventTag(): string[] | undefined { + return ["t", this.tag]; + } +} + +export class NostrLink implements ToNostrEventTag { constructor( readonly type: NostrPrefix, readonly id: string, @@ -42,8 +54,8 @@ export class NostrLink { toEventTag() { const relayEntry = this.relays ? [this.relays[0]] : []; - if (this.type === NostrPrefix.PublicKey) { - return ["p", this.id]; + if (this.type === NostrPrefix.PublicKey || this.type === NostrPrefix.Profile) { + return ["p", this.id, ...relayEntry]; } else if (this.type === NostrPrefix.Note || this.type === NostrPrefix.Event) { return ["e", this.id, ...relayEntry]; } else if (this.type === NostrPrefix.Address) { @@ -174,6 +186,18 @@ export class NostrLink { throw new Error(`Unknown tag kind ${tag[0]}`); } + static fromTags(tags: Array>) { + return removeUndefined( + tags.map(a => { + try { + return NostrLink.fromTag(a); + } catch { + // ignored, cant be mapped + } + }), + ); + } + static fromEvent(ev: TaggedNostrEvent | NostrEvent) { const relays = "relays" in ev ? ev.relays : undefined; diff --git a/packages/system/src/nostr.ts b/packages/system/src/nostr.ts index 50fd1934..039b1612 100644 --- a/packages/system/src/nostr.ts +++ b/packages/system/src/nostr.ts @@ -70,17 +70,6 @@ export type UserMetadata = { lud16?: string; }; -/** - * NIP-51 list types - */ -export enum Lists { - Muted = "mute", - Pinned = "pin", - Bookmarked = "bookmark", - Followed = "follow", - Badges = "profile_badges", -} - export interface FullRelaySettings { url: string; settings: RelaySettings;