diff --git a/.changeset/clean-horses-hang.md b/.changeset/clean-horses-hang.md new file mode 100644 index 000000000..d268982b4 --- /dev/null +++ b/.changeset/clean-horses-hang.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Fix delete events not getting published to outbox diff --git a/.changeset/neat-gorillas-tickle.md b/.changeset/neat-gorillas-tickle.md new file mode 100644 index 000000000..90417942e --- /dev/null +++ b/.changeset/neat-gorillas-tickle.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Improve list background loading diff --git a/package.json b/package.json index 994fb2158..31a9e8ea3 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "react-window": "^1.8.10", "remark-gfm": "^4.0.0", "remark-wiki-link": "^2.0.1", + "rx-nostr": "^3.4.1", "rxjs": "^7.8.1", "three": "^0.170.0", "three-spritetext": "^1.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9eb179716..6589dba0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -273,6 +273,9 @@ importers: remark-wiki-link: specifier: ^2.0.1 version: 2.0.1 + rx-nostr: + specifier: ^3.4.1 + version: 3.4.1 rxjs: specifier: ^7.8.1 version: 7.8.1 @@ -3314,6 +3317,9 @@ packages: typescript: optional: true + nostr-typedef@0.9.0: + resolution: {integrity: sha512-nLTzhlYcRnLQGUJ5YfvGAUDyGFHjGH6Qozltl/wV3UXelmiUwjrwI8IIxQNkbgVMv+zmbFi/m1xKHxIvVfG09w==} + nostr-wasm@0.1.0: resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==} @@ -3837,6 +3843,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rx-nostr@3.4.1: + resolution: {integrity: sha512-u/FkKUERrrJWVrDxMvhYCntRldyHS14CmHE9bRJefl8StU48x27MxsMDHIjZ1zDKtaOtBvvrpv+vCEaleIB/Mw==} + rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} @@ -8153,6 +8162,8 @@ snapshots: nostr-wasm: 0.1.0 typescript: 5.6.3 + nostr-typedef@0.9.0: {} + nostr-wasm@0.1.0: {} nth-check@2.1.1: @@ -8752,6 +8763,11 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rx-nostr@3.4.1: + dependencies: + nostr-typedef: 0.9.0 + rxjs: 7.8.1 + rxjs@7.8.1: dependencies: tslib: 2.8.1 diff --git a/src/classes/notifications.ts b/src/classes/notifications.ts index 031fb67dc..99d9e8644 100644 --- a/src/classes/notifications.ts +++ b/src/classes/notifications.ts @@ -1,5 +1,4 @@ import { NostrEvent, kinds, nip18, nip25 } from "nostr-tools"; -import _throttle from "lodash.throttle"; import { BehaviorSubject } from "rxjs"; import { map, throttleTime } from "rxjs/operators"; import { getZapPayment } from "applesauce-core/helpers"; @@ -10,7 +9,7 @@ import RelaySet from "./relay-set"; import clientRelaysService from "../services/client-relays"; import { getPubkeysMentionedInContent } from "../helpers/nostr/post"; import { TORRENT_COMMENT_KIND } from "../helpers/nostr/torrents"; -import { MUTE_LIST_KIND, getPubkeysFromList } from "../helpers/nostr/lists"; +import { getPubkeysFromList } from "../helpers/nostr/lists"; import { eventStore, queryStore } from "../services/event-store"; export const NotificationTypeSymbol = Symbol("notificationType"); @@ -121,7 +120,7 @@ export default class AccountNotifications { private filterEvent(event: CategorizedEvent) { // ignore if muted // TODO: this should be moved somewhere more performant - const muteList = eventStore.getReplaceable(MUTE_LIST_KIND, this.pubkey); + const muteList = eventStore.getReplaceable(kinds.Mutelist, this.pubkey); const mutedPubkeys = muteList ? getPubkeysFromList(muteList).map((p) => p.pubkey) : []; if (mutedPubkeys.includes(event.pubkey)) return false; diff --git a/src/components/common-menu-items/pin-event.tsx b/src/components/common-menu-items/pin-event.tsx index 6f1ae1fda..0a5b2aaef 100644 --- a/src/components/common-menu-items/pin-event.tsx +++ b/src/components/common-menu-items/pin-event.tsx @@ -2,12 +2,11 @@ import { useCallback, useState } from "react"; import { MenuItem } from "@chakra-ui/react"; import dayjs from "dayjs"; import { kinds } from "nostr-tools"; -import { getEventUID } from "nostr-idb"; import useCurrentAccount from "../../hooks/use-current-account"; import useUserPinList from "../../hooks/use-user-pin-list"; import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event"; -import { PIN_LIST_KIND, isEventInList, listAddEvent, listRemoveEvent } from "../../helpers/nostr/lists"; +import { isEventInList, listAddEvent, listRemoveEvent } from "../../helpers/nostr/lists"; import { PinIcon } from "../icons"; import { usePublishEvent } from "../../providers/global/publish-provider"; @@ -30,7 +29,7 @@ export default function PinEventMenuItem({ event }: { event: NostrEvent }) { const togglePin = useCallback(async () => { setLoading(true); let draft: DraftNostrEvent = { - kind: PIN_LIST_KIND, + kind: kinds.Pinlist, created_at: dayjs().unix(), content: list?.content ?? "", tags: list?.tags ? Array.from(list.tags) : [], diff --git a/src/components/embed-event/event-types/embedded-emoji-pack.tsx b/src/components/embed-event/event-types/embedded-emoji-pack.tsx index b559696bd..cfd07c48e 100644 --- a/src/components/embed-event/event-types/embedded-emoji-pack.tsx +++ b/src/components/embed-event/event-types/embedded-emoji-pack.tsx @@ -12,8 +12,8 @@ import { Text, } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; +import { getEmojis, getPackName } from "applesauce-core/helpers/emoji"; -import { getEmojis, getPackName } from "../../../helpers/nostr/emoji-packs"; import UserAvatarLink from "../../user/user-avatar-link"; import UserLink from "../../user/user-link"; import EmojiPackFavoriteButton from "../../../views/emoji-packs/components/emoji-pack-favorite-button"; diff --git a/src/components/embed-event/event-types/embedded-list.tsx b/src/components/embed-event/event-types/embedded-list.tsx index 020b82e01..64aa2d234 100644 --- a/src/components/embed-event/event-types/embedded-list.tsx +++ b/src/components/embed-event/event-types/embedded-list.tsx @@ -10,7 +10,7 @@ import { ListCardContent } from "../../../views/lists/components/list-card"; import { createCoordinate } from "../../../classes/batch-kind-pubkey-loader"; import { getSharableEventAddress } from "../../../services/event-relay-hint"; -export default function EmbeddedList({ list, ...props }: Omit & { list: NostrEvent }) { +export default function EmbeddedSetOrList({ list, ...props }: Omit & { list: NostrEvent }) { const link = isSpecialListKind(list.kind) ? createCoordinate(list.kind, list.pubkey) : getSharableEventAddress(list); const description = getListDescription(list); diff --git a/src/components/embed-event/index.tsx b/src/components/embed-event/index.tsx index 570aded32..b1dfddb4e 100644 --- a/src/components/embed-event/index.tsx +++ b/src/components/embed-event/index.tsx @@ -6,16 +6,7 @@ import { kinds } from "nostr-tools"; import EmbeddedNote from "./event-types/embedded-note"; import useSingleEvent from "../../hooks/use-single-event"; import { NostrEvent } from "../../types/nostr-event"; -import { GOAL_KIND } from "../../helpers/nostr/goal"; -import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs"; -import { - BOOKMARK_LIST_KIND, - CHANNELS_LIST_KIND, - COMMUNITIES_LIST_KIND, - NOTE_LIST_KIND, - PEOPLE_LIST_KIND, -} from "../../helpers/nostr/lists"; -import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities"; +import { LIST_KINDS, SET_KINDS } from "../../helpers/nostr/lists"; import { STEMSTR_TRACK_KIND } from "../../helpers/nostr/stemstr"; import { TORRENT_COMMENT_KIND, TORRENT_KIND } from "../../helpers/nostr/torrents"; import { FLARE_VIDEO_KIND } from "../../helpers/nostr/video"; @@ -25,9 +16,8 @@ import { safeDecode } from "../../helpers/nip19"; import type { EmbeddedGoalOptions } from "./event-types/embedded-goal"; import LoadingNostrLink from "../loading-nostr-link"; -import RelayCard from "../../views/relays/components/relay-card"; import EmbeddedRepost from "./event-types/embedded-repost"; -import EmbeddedList from "./event-types/embedded-list"; +import EmbeddedSetOrList from "./event-types/embedded-list"; import EmbeddedReaction from "./event-types/embedded-reaction"; import EmbeddedDM from "./event-types/embedded-dm"; import EmbeddedUnknown from "./event-types/embedded-unknown"; @@ -66,23 +56,17 @@ export function EmbedEvent({ return ; case kinds.LiveEvent: return ; - case GOAL_KIND: + case kinds.ZapGoal: return ; - case EMOJI_PACK_KIND: + case kinds.Emojisets: return ; - case PEOPLE_LIST_KIND: - case NOTE_LIST_KIND: - case BOOKMARK_LIST_KIND: - case COMMUNITIES_LIST_KIND: - case CHANNELS_LIST_KIND: - return ; case kinds.LongFormArticle: return ; case kinds.BadgeDefinition: return ; case kinds.LiveChatMessage: return ; - case COMMUNITY_DEFINITION_KIND: + case kinds.CommunityDefinition: return ; case STEMSTR_TRACK_KIND: return ; @@ -103,6 +87,9 @@ export function EmbedEvent({ return ; } + if (SET_KINDS.includes(event.kind) || LIST_KINDS.includes(event.kind)) + return ; + return ; }; diff --git a/src/components/note/bookmark-event.tsx b/src/components/note/bookmark-event.tsx index 49fde3dfc..0de3e4d46 100644 --- a/src/components/note/bookmark-event.tsx +++ b/src/components/note/bookmark-event.tsx @@ -11,20 +11,15 @@ import { MenuOptionGroup, useDisclosure, } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; import useCurrentAccount from "../../hooks/use-current-account"; -import useUserLists from "../../hooks/use-user-lists"; -import { - NOTE_LIST_KIND, - listAddEvent, - listRemoveEvent, - getEventPointersFromList, - getListName, -} from "../../helpers/nostr/lists"; +import useUserSets from "../../hooks/use-user-lists"; +import { listAddEvent, listRemoveEvent, getEventPointersFromList, getListName } from "../../helpers/nostr/lists"; import { NostrEvent } from "../../types/nostr-event"; import { getEventCoordinate } from "../../helpers/nostr/event"; import { BookmarkIcon, BookmarkedIcon, PlusCircleIcon } from "../icons"; -import NewListModal from "../../views/lists/components/new-list-modal"; +import NewSetModal from "../../views/lists/components/new-set-modal"; import useEventBookmarkActions from "../../hooks/use-event-bookmark-actions"; import { usePublishEvent } from "../../providers/global/publish-provider"; @@ -33,33 +28,37 @@ export default function BookmarkEventButton({ ...props }: { event: NostrEvent } & Omit) { const publish = usePublishEvent(); - const newListModal = useDisclosure(); + const newSetModal = useDisclosure(); const account = useCurrentAccount(); const [isLoading, setLoading] = useState(false); const { isLoading: loadingBookmark, toggleBookmark, isBookmarked } = useEventBookmarkActions(event); - const lists = useUserLists(account?.pubkey).filter((list) => list.kind === NOTE_LIST_KIND); + const bookmarkSets = useUserSets(account?.pubkey).filter( + (set) => set.kind === kinds.Genericlists || set.kind === kinds.Bookmarksets, + ); - const inLists = lists.filter((list) => getEventPointersFromList(list).some((p) => p.id === event.id)); + const inSets = bookmarkSets.filter((list) => getEventPointersFromList(list).some((p) => p.id === event.id)); const handleChange = useCallback( async (cords: string | string[]) => { if (!Array.isArray(cords)) return; setLoading(true); - const addToList = lists.find((list) => !inLists.includes(list) && cords.includes(getEventCoordinate(list))); - const removeFromList = lists.find((list) => inLists.includes(list) && !cords.includes(getEventCoordinate(list))); + const addToSet = bookmarkSets.find((set) => !inSets.includes(set) && cords.includes(getEventCoordinate(set))); + const removeFromSet = bookmarkSets.find( + (set) => inSets.includes(set) && !cords.includes(getEventCoordinate(set)), + ); - if (addToList) { - const draft = listAddEvent(addToList, event); + if (addToSet) { + const draft = listAddEvent(addToSet, event); await publish("Add to list", draft); - } else if (removeFromList) { - const draft = listRemoveEvent(removeFromList, event); + } else if (removeFromSet) { + const draft = listRemoveEvent(removeFromSet, event); await publish("Remove from list", draft); } setLoading(false); }, - [lists, event.id, publish], + [bookmarkSets, event.id, publish], ); return ( @@ -67,7 +66,7 @@ export default function BookmarkEventButton({ 0 || isBookmarked ? : } + icon={inSets.length > 0 || isBookmarked ? : } isDisabled={account?.readonly ?? true} {...props} /> @@ -80,37 +79,37 @@ export default function BookmarkEventButton({ Bookmark - {lists.length > 0 && ( + {bookmarkSets.length > 0 && ( getEventCoordinate(list))} + value={inSets.map((set) => getEventCoordinate(set))} onChange={handleChange} > - {lists.map((list) => ( + {bookmarkSets.map((set) => ( - {getListName(list)} + {getListName(set)} ))} )} - } onClick={newListModal.onOpen}> + } onClick={newSetModal.onOpen}> New list - {newListModal.isOpen && ( - )} diff --git a/src/components/people-list-selection/people-list-selection.tsx b/src/components/people-list-selection/people-list-selection.tsx index 2c2d0f79d..1e82a2761 100644 --- a/src/components/people-list-selection/people-list-selection.tsx +++ b/src/components/people-list-selection/people-list-selection.tsx @@ -15,11 +15,12 @@ import { useDisclosure, } from "@chakra-ui/react"; import { useObservable } from "applesauce-react/hooks"; +import { kinds } from "nostr-tools"; import { usePeopleListContext } from "../../providers/local/people-list-provider"; -import useUserLists from "../../hooks/use-user-lists"; +import useUserSets from "../../hooks/use-user-lists"; import useCurrentAccount from "../../hooks/use-current-account"; -import { PEOPLE_LIST_KIND, getListName, getPubkeysFromList } from "../../helpers/nostr/lists"; +import { getListName, getPubkeysFromList } from "../../helpers/nostr/lists"; import { getEventCoordinate, getEventUID } from "../../helpers/nostr/event"; import useFavoriteLists from "../../hooks/use-favorite-lists"; import { NostrEvent } from "../../types/nostr-event"; @@ -58,7 +59,7 @@ export default function PeopleListSelection({ } & Omit) { const modal = useDisclosure(); const account = useCurrentAccount(); - const lists = useUserLists(account?.pubkey); + const lists = useUserSets(account?.pubkey); const { lists: favoriteLists } = useFavoriteLists(); const { selected, setSelected, listEvent } = usePeopleListContext(); @@ -128,7 +129,7 @@ export default function PeopleListSelection({ {lists - .filter((l) => l.kind === PEOPLE_LIST_KIND) + .filter((l) => l.kind === kinds.Followsets) .map((list) => ( selectList(list)} /> ))} diff --git a/src/components/reaction-picker.tsx b/src/components/reaction-picker.tsx index fbecc6284..11a8577ec 100644 --- a/src/components/reaction-picker.tsx +++ b/src/components/reaction-picker.tsx @@ -1,9 +1,10 @@ import { Divider, Flex, IconButton, Image, Text } from "@chakra-ui/react"; +import { getEmojis, getPackName } from "applesauce-core/helpers/emoji"; import { DislikeIcon, LikeIcon } from "./icons"; import useCurrentAccount from "../hooks/use-current-account"; import useReplaceableEvent from "../hooks/use-replaceable-event"; -import { getEmojis, getPackCordsFromFavorites, getPackName } from "../helpers/nostr/emoji-packs"; +import { getPackCordsFromFavorites } from "../helpers/nostr/emoji-packs"; import useFavoriteEmojiPacks from "../hooks/use-favorite-emoji-packs"; import useAppSettings from "../hooks/use-app-settings"; diff --git a/src/components/user/user-follow-button.tsx b/src/components/user/user-follow-button.tsx index f6b972a1d..33d63a935 100644 --- a/src/components/user/user-follow-button.tsx +++ b/src/components/user/user-follow-button.tsx @@ -11,12 +11,12 @@ import { MenuDivider, useDisclosure, } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; import useCurrentAccount from "../../hooks/use-current-account"; import { ChevronDownIcon, FollowIcon, MuteIcon, PlusCircleIcon, UnfollowIcon, UnmuteIcon } from "../icons"; -import useUserLists from "../../hooks/use-user-lists"; +import useUserSets from "../../hooks/use-user-lists"; import { - PEOPLE_LIST_KIND, createEmptyContactList, listAddPerson, listRemovePerson, @@ -28,7 +28,7 @@ import { getEventCoordinate } from "../../helpers/nostr/event"; import { useSigningContext } from "../../providers/global/signing-provider"; import useUserContactList from "../../hooks/use-user-contact-list"; import useAsyncErrorHandler from "../../hooks/use-async-error-handler"; -import NewListModal from "../../views/lists/components/new-list-modal"; +import NewSetModal from "../../views/lists/components/new-set-modal"; import useUserMuteActions from "../../hooks/use-user-mute-actions"; import { useMuteModalContext } from "../../providers/route/mute-modal-provider"; import { usePublishEvent } from "../../providers/global/publish-provider"; @@ -40,7 +40,7 @@ function UsersLists({ pubkey }: { pubkey: string }) { const [isLoading, setLoading] = useState(false); const newListModal = useDisclosure(); - const lists = useUserLists(account.pubkey).filter((list) => list.kind === PEOPLE_LIST_KIND); + const lists = useUserSets(account.pubkey).filter((list) => list.kind === kinds.Followsets); const inLists = lists.filter((list) => getPubkeysFromList(list).some((p) => p.pubkey === pubkey)); @@ -93,7 +93,7 @@ function UsersLists({ pubkey }: { pubkey: string }) { New list - {newListModal.isOpen && } + {newListModal.isOpen && } ); } diff --git a/src/helpers/nostr/communities.ts b/src/helpers/nostr/communities.ts index 42c5d5dd2..8f8e4cbf0 100644 --- a/src/helpers/nostr/communities.ts +++ b/src/helpers/nostr/communities.ts @@ -5,8 +5,6 @@ import { parseCoordinate } from "./event"; /** @deprecated */ export const SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER = "communities"; -export const COMMUNITY_DEFINITION_KIND = kinds.CommunityDefinition; -export const COMMUNITY_APPROVAL_KIND = kinds.CommunityPostApproval; export function getCommunityName(community: NostrEvent) { const name = community.tags.find(isDTag)?.[1]; @@ -71,7 +69,7 @@ export function validateCommunity(community: NostrEvent) { export function buildApprovalMap(events: Iterable, mods: string[]) { const approvals = new Map(); for (const event of events) { - if (event.kind === COMMUNITY_APPROVAL_KIND && mods.includes(event.pubkey)) { + if (event.kind === kinds.CommunityPostApproval && mods.includes(event.pubkey)) { for (const tag of event.tags) { if (isETag(tag)) { const arr = approvals.get(tag[1]); @@ -85,6 +83,6 @@ export function buildApprovalMap(events: Iterable, mods: string[]) { } export function getEventCommunityPointer(event: NostrEvent) { - const communityTag = event.tags.filter(isATag).find((t) => t[1].startsWith(COMMUNITY_DEFINITION_KIND + ":")); + const communityTag = event.tags.filter(isATag).find((t) => t[1].startsWith(kinds.CommunityDefinition + ":")); return communityTag ? parseCoordinate(communityTag[1], true) : null; } diff --git a/src/helpers/nostr/emoji-packs.ts b/src/helpers/nostr/emoji-packs.ts index cd955ee57..9d602917d 100644 --- a/src/helpers/nostr/emoji-packs.ts +++ b/src/helpers/nostr/emoji-packs.ts @@ -1,19 +1,6 @@ import { getTagValue } from "applesauce-core/helpers"; import { NostrEvent, isATag } from "../../types/nostr-event"; -export const EMOJI_PACK_KIND = 30030; -export const USER_EMOJI_LIST_KIND = 10030; - -export function getPackName(pack: NostrEvent) { - return getTagValue(pack, "title") || getTagValue(pack, "d"); -} - -export function getEmojis(pack: NostrEvent) { - return pack.tags - .filter((t) => t[0] === "emoji" && t[1] && t[2]) - .map((t) => ({ name: t[1] as string, url: t[2] as string })); -} - export function getPackCordsFromFavorites(event: NostrEvent) { return event.tags.filter(isATag).map((t) => t[1]); } diff --git a/src/helpers/nostr/lists.ts b/src/helpers/nostr/lists.ts index 67287c7d1..0e7018410 100644 --- a/src/helpers/nostr/lists.ts +++ b/src/helpers/nostr/lists.ts @@ -6,25 +6,41 @@ import { PTag, isATag, isDTag, isPTag, isRTag } from "../../types/nostr-event"; import { getEventCoordinate, replaceOrAddSimpleTag } from "./event"; import { getRelayVariations, safeRelayUrls } from "../relay"; -export const MUTE_LIST_KIND = kinds.Mutelist; -export const PIN_LIST_KIND = kinds.Pinlist; -export const BOOKMARK_LIST_KIND = kinds.BookmarkList; -export const COMMUNITIES_LIST_KIND = kinds.CommunitiesList; -export const CHANNELS_LIST_KIND = kinds.PublicChatsList; - -export const PEOPLE_LIST_KIND = kinds.Followsets; -export const NOTE_LIST_KIND = 30001; -export const BOOKMARK_LIST_SET_KIND = kinds.Bookmarksets; +export const LIST_KINDS = [ + kinds.Mutelist, + kinds.Pinlist, + kinds.RelayList, + kinds.BookmarkList, + kinds.CommunitiesList, + kinds.PublicChatsList, + kinds.BlockedRelaysList, + kinds.SearchRelaysList, + kinds.InterestsList, + kinds.UserEmojiList, + kinds.DirectMessageRelaysList, +]; +export const SET_KINDS = [ + kinds.Followsets, + kinds.Bookmarksets, + kinds.Genericlists, + kinds.Relaysets, + kinds.Interestsets, + kinds.Emojisets, + kinds.Curationsets, +]; export function getListName(event: NostrEvent) { if (event.kind === kinds.Contacts) return "Following"; - if (event.kind === MUTE_LIST_KIND) return "Mute"; - if (event.kind === PIN_LIST_KIND) return "Pins"; - if (event.kind === BOOKMARK_LIST_KIND) return "Bookmarks"; - if (event.kind === COMMUNITIES_LIST_KIND) return "Communities"; + if (event.kind === kinds.Mutelist) return "Mute"; + if (event.kind === kinds.Pinlist) return "Pins"; + if (event.kind === kinds.BookmarkList) return "Bookmarks"; + if (event.kind === kinds.CommunitiesList) return "Communities"; + if (event.kind === kinds.InterestsList) return "Interests"; + if (event.kind === kinds.PublicChatsList) return "Public Chats"; + return ( - event.tags.find((t) => t[0] === "name")?.[1] || event.tags.find((t) => t[0] === "title")?.[1] || + event.tags.find((t) => t[0] === "name")?.[1] || event.tags.find(isDTag)?.[1] ); } @@ -41,18 +57,13 @@ export function setListDescription(draft: EventTemplate, description: string) { export function isJunkList(event: NostrEvent) { const name = event.tags.find(isDTag)?.[1]; if (!name) return false; - if (event.kind !== PEOPLE_LIST_KIND) return false; + if (event.kind !== kinds.Followsets) return false; return /^(chats\/([0-9a-f]{64}|null)|notifications)\/lastOpened$/.test(name); } + +/** Check if is kind is list */ export function isSpecialListKind(kind: number) { - return ( - kind === kinds.Contacts || - kind === MUTE_LIST_KIND || - kind === PIN_LIST_KIND || - kind === BOOKMARK_LIST_KIND || - kind === COMMUNITIES_LIST_KIND || - kind === CHANNELS_LIST_KIND - ); + return kind === kinds.Contacts || LIST_KINDS.includes(kind); } export function cloneList(list: NostrEvent, keepCreatedAt = false): EventTemplate { diff --git a/src/helpers/nostr/mute-list.ts b/src/helpers/nostr/mute-list.ts index 582144b7d..46cc5533a 100644 --- a/src/helpers/nostr/mute-list.ts +++ b/src/helpers/nostr/mute-list.ts @@ -1,6 +1,7 @@ import dayjs from "dayjs"; import { DraftNostrEvent, NostrEvent, isPTag } from "../../types/nostr-event"; -import { MUTE_LIST_KIND, getPubkeysFromList, isPubkeyInList, listAddPerson, listRemovePerson } from "./lists"; +import { getPubkeysFromList, isPubkeyInList, listAddPerson, listRemovePerson } from "./lists"; +import { kinds } from "nostr-tools"; export function getPubkeysFromMuteList(muteList: NostrEvent | DraftNostrEvent) { const expirations = getPubkeysExpiration(muteList); @@ -37,7 +38,7 @@ export function createEmptyMuteList(): DraftNostrEvent { created_at: dayjs().unix(), content: "", tags: [], - kind: MUTE_LIST_KIND, + kind: kinds.Mutelist, }; } @@ -70,7 +71,7 @@ export function pruneExpiredPubkeys(muteList: NostrEvent | DraftNostrEvent) { const expirations = getPubkeysExpiration(muteList); const now = dayjs().unix(); const draft: DraftNostrEvent = { - kind: MUTE_LIST_KIND, + kind: kinds.Mutelist, content: muteList.content, created_at: now, tags: muteList.tags.filter((tag) => { diff --git a/src/hooks/use-count-community-members.ts b/src/hooks/use-count-community-members.ts index 9c7262e28..20dc14144 100644 --- a/src/hooks/use-count-community-members.ts +++ b/src/hooks/use-count-community-members.ts @@ -1,8 +1,9 @@ +import { kinds } from "nostr-tools"; + import { getEventCoordinate } from "../helpers/nostr/event"; -import { COMMUNITIES_LIST_KIND } from "../helpers/nostr/lists"; import { NostrEvent } from "../types/nostr-event"; import useEventCount from "./use-event-count"; export default function useCountCommunityMembers(community: NostrEvent) { - return useEventCount({ "#a": [getEventCoordinate(community)], kinds: [COMMUNITIES_LIST_KIND] }); + return useEventCount({ "#a": [getEventCoordinate(community)], kinds: [kinds.CommunitiesList] }); } diff --git a/src/hooks/use-event-bookmark-actions.ts b/src/hooks/use-event-bookmark-actions.ts index 5c78f5026..9585fd28c 100644 --- a/src/hooks/use-event-bookmark-actions.ts +++ b/src/hooks/use-event-bookmark-actions.ts @@ -1,17 +1,12 @@ import { useCallback, useState } from "react"; +import { kinds } from "nostr-tools"; import dayjs from "dayjs"; import { DraftNostrEvent, NostrEvent } from "../types/nostr-event"; import { useSigningContext } from "../providers/global/signing-provider"; import userUserBookmarksList from "./use-user-bookmarks-list"; import { getEventCoordinate, isReplaceable, pointerMatchEvent } from "../helpers/nostr/event"; -import { - BOOKMARK_LIST_KIND, - listAddCoordinate, - listAddEvent, - listRemoveCoordinate, - listRemoveEvent, -} from "../helpers/nostr/lists"; +import { listAddCoordinate, listAddEvent, listRemoveCoordinate, listRemoveEvent } from "../helpers/nostr/lists"; import { usePublishEvent } from "../providers/global/publish-provider"; export default function useEventBookmarkActions(event: NostrEvent) { @@ -28,7 +23,7 @@ export default function useEventBookmarkActions(event: NostrEvent) { const removeBookmark = useCallback(async () => { setLoading(true); let draft: DraftNostrEvent = { - kind: BOOKMARK_LIST_KIND, + kind: kinds.BookmarkList, content: bookmarkList?.content ?? "", tags: bookmarkList?.tags ?? [], created_at: dayjs().unix(), @@ -46,7 +41,7 @@ export default function useEventBookmarkActions(event: NostrEvent) { const addBookmark = useCallback(async () => { setLoading(true); let draft: DraftNostrEvent = { - kind: BOOKMARK_LIST_KIND, + kind: kinds.BookmarkList, content: bookmarkList?.content ?? "", tags: bookmarkList?.tags ?? [], created_at: dayjs().unix(), diff --git a/src/hooks/use-favorite-emoji-packs.ts b/src/hooks/use-favorite-emoji-packs.ts index fde9a56ca..e90a9f3f1 100644 --- a/src/hooks/use-favorite-emoji-packs.ts +++ b/src/hooks/use-favorite-emoji-packs.ts @@ -1,6 +1,7 @@ +import { kinds } from "nostr-tools"; + import useReplaceableEvent from "./use-replaceable-event"; import useCurrentAccount from "./use-current-account"; -import { USER_EMOJI_LIST_KIND } from "../helpers/nostr/emoji-packs"; import { RequestOptions } from "../services/replaceable-events"; export const FAVORITE_LISTS_IDENTIFIER = "nostrudel-favorite-lists"; @@ -13,7 +14,7 @@ export default function useFavoriteEmojiPacks( const account = useCurrentAccount(); const key = pubkey || account?.pubkey; const favoritePacks = useReplaceableEvent( - key ? { kind: USER_EMOJI_LIST_KIND, pubkey: key } : undefined, + key ? { kind: kinds.Emojisets, pubkey: key } : undefined, additionalRelays, opts, ); diff --git a/src/hooks/use-user-bookmarks-list.ts b/src/hooks/use-user-bookmarks-list.ts index 0893aef8d..80df2fb4a 100644 --- a/src/hooks/use-user-bookmarks-list.ts +++ b/src/hooks/use-user-bookmarks-list.ts @@ -1,6 +1,7 @@ import { useMemo } from "react"; +import { kinds } from "nostr-tools"; -import { BOOKMARK_LIST_KIND, getAddressPointersFromList, getEventPointersFromList } from "../helpers/nostr/lists"; +import { getAddressPointersFromList, getEventPointersFromList } from "../helpers/nostr/lists"; import { RequestOptions } from "../services/replaceable-events"; import useCurrentAccount from "./use-current-account"; import useReplaceableEvent from "./use-replaceable-event"; @@ -9,7 +10,7 @@ export default function userUserBookmarksList(pubkey?: string, relays: string[] const account = useCurrentAccount(); const key = pubkey ?? account?.pubkey; - const list = useReplaceableEvent(key ? { kind: BOOKMARK_LIST_KIND, pubkey: key } : undefined, relays, opts); + const list = useReplaceableEvent(key ? { kind: kinds.BookmarkList, pubkey: key } : undefined, relays, opts); const addressPointers = useMemo(() => (list ? getAddressPointersFromList(list) : []), [list]); const eventPointers = useMemo(() => (list ? getEventPointersFromList(list) : []), [list]); diff --git a/src/hooks/use-user-channels-list.ts b/src/hooks/use-user-channels-list.ts index 0fc0ace92..eed644ba0 100644 --- a/src/hooks/use-user-channels-list.ts +++ b/src/hooks/use-user-channels-list.ts @@ -1,4 +1,5 @@ -import { CHANNELS_LIST_KIND, getEventPointersFromList } from "../helpers/nostr/lists"; +import { kinds } from "nostr-tools"; +import { getEventPointersFromList } from "../helpers/nostr/lists"; import { RequestOptions } from "../services/replaceable-events"; import useCurrentAccount from "./use-current-account"; import useReplaceableEvent from "./use-replaceable-event"; @@ -7,7 +8,7 @@ export default function useUserChannelsList(pubkey?: string, relays: string[] = const account = useCurrentAccount(); const key = pubkey ?? account?.pubkey; - const list = useReplaceableEvent(key ? { kind: CHANNELS_LIST_KIND, pubkey: key } : undefined, relays, opts); + const list = useReplaceableEvent(key ? { kind: kinds.PublicChatsList, pubkey: key } : undefined, relays, opts); const pointers = list ? getEventPointersFromList(list) : []; diff --git a/src/hooks/use-user-communities-list.ts b/src/hooks/use-user-communities-list.ts index 565b3336d..c7d392375 100644 --- a/src/hooks/use-user-communities-list.ts +++ b/src/hooks/use-user-communities-list.ts @@ -1,5 +1,7 @@ -import { COMMUNITY_DEFINITION_KIND, SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER } from "../helpers/nostr/communities"; -import { COMMUNITIES_LIST_KIND, NOTE_LIST_KIND, getAddressPointersFromList } from "../helpers/nostr/lists"; +import { kinds } from "nostr-tools"; + +import { SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER } from "../helpers/nostr/communities"; +import { getAddressPointersFromList } from "../helpers/nostr/lists"; import { RequestOptions } from "../services/replaceable-events"; import useCurrentAccount from "./use-current-account"; import useReplaceableEvent from "./use-replaceable-event"; @@ -14,7 +16,7 @@ export default function useUserCommunitiesList(pubkey?: string, relays?: Iterabl const oldList = useReplaceableEvent( key ? { - kind: NOTE_LIST_KIND, + kind: kinds.Genericlists, identifier: SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER, pubkey: key, } @@ -22,7 +24,7 @@ export default function useUserCommunitiesList(pubkey?: string, relays?: Iterabl [], opts, ); - const list = useReplaceableEvent(key ? { kind: COMMUNITIES_LIST_KIND, pubkey: key } : undefined, relays, opts); + const list = useReplaceableEvent(key ? { kind: kinds.CommunitiesList, pubkey: key } : undefined, relays, opts); let useList = list || oldList; @@ -32,7 +34,7 @@ export default function useUserCommunitiesList(pubkey?: string, relays?: Iterabl } const pointers = useList - ? getAddressPointersFromList(useList).filter((cord) => cord.kind === COMMUNITY_DEFINITION_KIND) + ? getAddressPointersFromList(useList).filter((cord) => cord.kind === kinds.CommunityDefinition) : []; return { list: useList, pointers }; diff --git a/src/hooks/use-user-lists.ts b/src/hooks/use-user-lists.ts index 1e379f391..fffcf2f90 100644 --- a/src/hooks/use-user-lists.ts +++ b/src/hooks/use-user-lists.ts @@ -1,28 +1,29 @@ -import { useCallback } from "react"; +import { useEffect } from "react"; +import { useStoreQuery } from "applesauce-react/hooks"; +import { TimelineQuery } from "applesauce-core/queries"; -import { NOTE_LIST_KIND, PEOPLE_LIST_KIND, isJunkList } from "../helpers/nostr/lists"; +import { SET_KINDS, isJunkList } from "../helpers/nostr/lists"; import { useReadRelays } from "./use-client-relays"; -import useTimelineLoader from "./use-timeline-loader"; -import { NostrEvent } from "../types/nostr-event"; -import { truncateId } from "../helpers/string"; +import userSetsService from "../services/user-sets"; -export default function useUserLists(pubkey?: string, additionalRelays?: Iterable) { +export default function useUserSets(pubkey?: string, additionalRelays?: Iterable, alwaysRequest?: boolean) { const readRelays = useReadRelays(additionalRelays); - const eventFilter = useCallback((event: NostrEvent) => { - return !isJunkList(event); - }, []); - const { timeline } = useTimelineLoader( - `${truncateId(pubkey ?? "anon")}-lists`, - readRelays, - pubkey - ? { - authors: [pubkey], - kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND], - } - : undefined, - { eventFilter }, + useEffect(() => { + if (pubkey) userSetsService.requestSets(pubkey, readRelays, alwaysRequest); + }, [pubkey, readRelays.urls.join("|"), alwaysRequest]); + + return ( + useStoreQuery( + TimelineQuery, + pubkey + ? [ + { + authors: [pubkey], + kinds: SET_KINDS, + }, + ] + : undefined, + )?.filter((e) => !isJunkList(e)) ?? [] ); - - return timeline; } diff --git a/src/hooks/use-user-mute-list.ts b/src/hooks/use-user-mute-list.ts index aaa72f079..b3f2bb29c 100644 --- a/src/hooks/use-user-mute-list.ts +++ b/src/hooks/use-user-mute-list.ts @@ -1,5 +1,5 @@ +import { kinds } from "nostr-tools"; import useReplaceableEvent from "./use-replaceable-event"; -import { MUTE_LIST_KIND } from "../helpers/nostr/lists"; import { RequestOptions } from "../services/replaceable-events"; export default function useUserMuteList( @@ -7,5 +7,5 @@ export default function useUserMuteList( additionalRelays?: Iterable, opts: RequestOptions = {}, ) { - return useReplaceableEvent(pubkey && { kind: MUTE_LIST_KIND, pubkey }, additionalRelays, opts); + return useReplaceableEvent(pubkey && { kind: kinds.Mutelist, pubkey }, additionalRelays, opts); } diff --git a/src/hooks/use-user-mute-lists.ts b/src/hooks/use-user-mute-lists.ts index 49b30b239..a2b21897f 100644 --- a/src/hooks/use-user-mute-lists.ts +++ b/src/hooks/use-user-mute-lists.ts @@ -1,7 +1,8 @@ import { useMemo } from "react"; +import { kinds } from "nostr-tools"; import useReplaceableEvent from "./use-replaceable-event"; -import { PEOPLE_LIST_KIND, getPubkeysFromList } from "../helpers/nostr/lists"; +import { getPubkeysFromList } from "../helpers/nostr/lists"; import useUserMuteList from "./use-user-mute-list"; import { RequestOptions } from "../services/replaceable-events"; @@ -12,7 +13,7 @@ export default function useUserMuteLists( ) { const muteList = useUserMuteList(pubkey, additionalRelays, opts); const altMuteList = useReplaceableEvent( - pubkey && { kind: PEOPLE_LIST_KIND, pubkey, identifier: "mute" }, + pubkey && { kind: kinds.Followsets, pubkey, identifier: "mute" }, additionalRelays, opts, ); diff --git a/src/hooks/use-user-pin-list.ts b/src/hooks/use-user-pin-list.ts index 0102f117f..4732363cd 100644 --- a/src/hooks/use-user-pin-list.ts +++ b/src/hooks/use-user-pin-list.ts @@ -1,4 +1,5 @@ -import { PIN_LIST_KIND, getPointersFromList } from "../helpers/nostr/lists"; +import { kinds } from "nostr-tools"; +import { getPointersFromList } from "../helpers/nostr/lists"; import { RequestOptions } from "../services/replaceable-events"; import useCurrentAccount from "./use-current-account"; import useReplaceableEvent from "./use-replaceable-event"; @@ -7,7 +8,7 @@ export default function useUserPinList(pubkey?: string, relays: string[] = [], o const account = useCurrentAccount(); const key = pubkey ?? account?.pubkey; - const list = useReplaceableEvent(key ? { kind: PIN_LIST_KIND, pubkey: key } : undefined, relays, opts); + const list = useReplaceableEvent(key ? { kind: kinds.Pinlist, pubkey: key } : undefined, relays, opts); const pointers = list ? getPointersFromList(list) : []; diff --git a/src/providers/route/delete-event-provider.tsx b/src/providers/route/delete-event-provider.tsx index 9c3c5ac43..ad54ba5ce 100644 --- a/src/providers/route/delete-event-provider.tsx +++ b/src/providers/route/delete-event-provider.tsx @@ -30,6 +30,8 @@ import { Tag } from "../../types/nostr-event"; import { EmbedEvent } from "../../components/embed-event"; import { useWriteRelays } from "../../hooks/use-client-relays"; import { usePublishEvent } from "../global/publish-provider"; +import { useUserOutbox } from "../../hooks/use-user-mailboxes"; +import useCurrentAccount from "../../hooks/use-current-account"; type DeleteEventContextType = { isLoading: boolean; @@ -46,13 +48,15 @@ export function useDeleteEventContext() { } export default function DeleteEventProvider({ children }: PropsWithChildren) { + const account = useCurrentAccount(); const publish = usePublishEvent(); const [isLoading, setLoading] = useState(false); const [event, setEvent] = useState(); const [defer, setDefer] = useState>(); const [reason, setReason] = useState(""); - const writeRelays = useWriteRelays(); + const outbox = useUserOutbox(account?.pubkey); + const writeRelays = useWriteRelays(outbox); const deleteEvent = useCallback((event: Event) => { setEvent(event); @@ -77,7 +81,7 @@ export default function DeleteEventProvider({ children }: PropsWithChildren) { content: reason, created_at: dayjs().unix(), }; - await publish("Delete", draft, undefined, false); + await publish("Delete", draft); defer?.resolve(); } catch (e) { defer?.reject(); diff --git a/src/services/db/schema.ts b/src/services/db/schema.ts index 100a03193..133ed6872 100644 --- a/src/services/db/schema.ts +++ b/src/services/db/schema.ts @@ -1,7 +1,4 @@ -import { DBSchema } from "idb"; - import { NostrEvent } from "../../types/nostr-event"; -import { RelayInformationDocument } from "../relay-info"; import { AppSettings } from "../../helpers/app-settings"; export interface SchemaV1 { @@ -30,7 +27,7 @@ export interface SchemaV1 { value: { name: string; domain: string; pubkey: string; relays: string[]; updated: number }; indexes: { name: string; domain: string; pubkey: string; updated: number }; }; - relayInfo: { key: string; value: RelayInformationDocument }; + relayInfo: { key: string; value: any }; relayScoreboardStats: { key: string; value: { diff --git a/src/services/relay-info.ts b/src/services/relay-info.ts index accd22d32..85306a3ed 100644 --- a/src/services/relay-info.ts +++ b/src/services/relay-info.ts @@ -1,79 +1,43 @@ -import db from "./db"; -import { fetchWithProxy } from "../helpers/request"; -import { isHexKey } from "../helpers/nip19"; -import { validateRelayURL } from "../helpers/relay"; import { AbstractRelay } from "nostr-tools/abstract-relay"; +import { Nip11Registry } from "rx-nostr"; -export type RelayInformationDocument = { - name: string; - description: string; - icon?: string; - pubkey?: string; - contact: string; - supported_nips?: number[]; - software: string; - version: string; - payments_url?: string; -}; +import db from "./db"; +import { logger } from "../helpers/debug"; -function sanitizeInfo(info: RelayInformationDocument) { - if (info.pubkey && !isHexKey(info.pubkey)) { - delete info.pubkey; +const log = logger.extend("Nip11Registry"); + +const tx = db.transaction("relayInfo", "readonly"); +let loaded = 0; +let cursor = await tx.objectStore("relayInfo").openCursor(); +while (cursor) { + try { + Nip11Registry.set(cursor.key, cursor.value); + loaded++; + } catch (error) {} + cursor = await cursor.continue(); +} + +log(`Loaded ${loaded} relay info`); + +async function getInfo(relay: string | AbstractRelay, alwaysFetch = false) { + relay = typeof relay === "string" ? relay : relay.url; + + let info = Nip11Registry.get(relay); + + if (!info || alwaysFetch) { + info = await Nip11Registry.fetch(relay); + db.put("relayInfo", info, relay); } return info; } -async function fetchInfo(relay: string) { - const url = validateRelayURL(relay); - url.protocol = url.protocol === "ws:" ? "http" : "https"; - - const infoDoc = await fetchWithProxy(url, { headers: { Accept: "application/nostr+json" } }).then( - (res) => res.json() as Promise, - ); - - sanitizeInfo(infoDoc); - - memoryCache.set(relay, infoDoc); - await db.put("relayInfo", infoDoc, relay); - - return infoDoc; -} - -const memoryCache = new Map(); -async function getInfo(relay: string, alwaysFetch = false) { - const url = validateRelayURL(relay).toString(); - if (memoryCache.has(url) && !alwaysFetch) return memoryCache.get(url)!; - - const cached = await db.get("relayInfo", url); - if (cached && !alwaysFetch) { - memoryCache.set(url, cached); - return cached as RelayInformationDocument; - } - - return fetchInfo(relay); -} - -const pending: Record | undefined> = {}; -function dedupedGetIdentity(relay: string | AbstractRelay, alwaysFetch = false) { - relay = typeof relay === "string" ? relay : relay.url; - - const request = pending[relay]; - if (request) return request; - return (pending[relay] = getInfo(relay, alwaysFetch).then((v) => { - delete pending[relay]; - return v; - })); -} - -export const relayInfoService = { - cache: memoryCache, - fetchInfo, - getInfo: dedupedGetIdentity, -}; +export const relayInfoService = { getInfo }; if (import.meta.env.DEV) { // @ts-ignore window.relayInfoService = relayInfoService; + // @ts-ignore + window.Nip11Registry = Nip11Registry; } export default relayInfoService; diff --git a/src/services/user-event-sync.ts b/src/services/user-event-sync.ts index 5e9050f2f..14004565e 100644 --- a/src/services/user-event-sync.ts +++ b/src/services/user-event-sync.ts @@ -1,7 +1,9 @@ import { kinds } from "nostr-tools"; import _throttle from "lodash.throttle"; import { combineLatest, distinct, filter } from "rxjs"; +import { AbstractRelay } from "nostr-tools/abstract-relay"; import { USER_BLOSSOM_SERVER_LIST_KIND } from "blossom-client-sdk"; +import { isFromCache } from "applesauce-core/helpers"; import { COMMON_CONTACT_RELAYS } from "../const"; import { logger } from "../helpers/debug"; @@ -10,13 +12,18 @@ import clientRelaysService from "./client-relays"; import { offlineMode } from "./offline-mode"; import replaceableEventsService from "./replaceable-events"; import { APP_SETTING_IDENTIFIER, APP_SETTINGS_KIND } from "./user-app-settings"; -import { queryStore } from "./event-store"; +import { eventStore, queryStore } from "./event-store"; import { Account } from "../classes/accounts/account"; +import { MultiSubscription } from "applesauce-net/subscription"; +import relayPoolService from "./relay-pool"; +import { localRelay } from "./local-relay"; const log = logger.extend("UserEventSync"); function downloadEvents(account: Account) { const relays = clientRelaysService.readRelays.value; + const cleanup: (() => void)[] = []; + const requestReplaceable = (relays: Iterable, kind: number, d?: string) => { replaceableEventsService.requestEvent(relays, kind, account.pubkey, d, { alwaysRequest: true, @@ -43,9 +50,29 @@ function downloadEvents(account: Account) { alwaysRequest: true, }, ); + + if (mailboxes?.outboxes && mailboxes.outboxes.length > 0) { + log(`Loading delete events`); + const sub = new MultiSubscription(relayPoolService); + sub.setRelays( + localRelay + ? [...mailboxes.outboxes.map((r) => relayPoolService.requestRelay(r)), localRelay as AbstractRelay] + : mailboxes.outboxes, + ); + sub.setFilters([{ kinds: [kinds.EventDeletion], authors: [account.pubkey] }]); + + sub.open(); + sub.onEvent.subscribe((e) => { + eventStore.add(e); + if (!isFromCache(e) && localRelay) localRelay.publish(e); + }); + + cleanup.push(() => sub.close()); + } }); return () => { + for (const fn of cleanup) fn(); mailboxesSub.unsubscribe(); }; } diff --git a/src/services/user-sets.ts b/src/services/user-sets.ts new file mode 100644 index 000000000..cb3fb43df --- /dev/null +++ b/src/services/user-sets.ts @@ -0,0 +1,58 @@ +import { AbstractRelay } from "nostr-tools/abstract-relay"; + +import SuperMap from "../classes/super-map"; +import { localRelay } from "./local-relay"; +import relayPoolService from "./relay-pool"; +import Process from "../classes/process"; +import { LightningIcon } from "../components/icons"; +import processManager from "./process-manager"; +import { logger } from "../helpers/debug"; +import BatchKindPubkeyLoader from "../classes/batch-kind-pubkey-loader"; +import { eventStore } from "./event-store"; +import { SET_KINDS } from "../helpers/nostr/lists"; + +class UserSetsService { + log = logger.extend("UserSetsService"); + process: Process; + + private loaded = new Map(); + loaders = new SuperMap((relay) => { + const loader = new BatchKindPubkeyLoader(eventStore, relay, this.log.extend(relay.url)); + this.process.addChild(loader.process); + return loader; + }); + + constructor() { + this.process = new Process("UserSetsService", this); + this.process.icon = LightningIcon; + this.process.active = true; + + processManager.registerProcess(this.process); + } + + requestSets(pubkey: string, urls: Iterable, alwaysRequest = true) { + if (this.loaded.get(pubkey) && !alwaysRequest) return; + + const loaders: BatchKindPubkeyLoader[] = []; + + if (localRelay) loaders.push(this.loaders.get(localRelay as AbstractRelay)); + + const relays = relayPoolService.getRelays(urls); + for (const relay of relays) loaders.push(this.loaders.get(relay)); + + for (const loader of loaders) { + for (const kind of SET_KINDS) { + loader.requestEvent(kind, pubkey); + } + } + } +} + +const userSetsService = new UserSetsService(); + +if (import.meta.env.DEV) { + // @ts-ignore + window.userSetsService = userSetsService; +} + +export default userSetsService; diff --git a/src/views/channels/components/channel-join-button.tsx b/src/views/channels/components/channel-join-button.tsx index 56ec3bfc9..79dad70d9 100644 --- a/src/views/channels/components/channel-join-button.tsx +++ b/src/views/channels/components/channel-join-button.tsx @@ -1,10 +1,11 @@ import { useCallback } from "react"; import dayjs from "dayjs"; +import { kinds } from "nostr-tools"; import { Button, ButtonProps } from "@chakra-ui/react"; import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event"; import useCurrentAccount from "../../../hooks/use-current-account"; -import { CHANNELS_LIST_KIND, listAddEvent, listRemoveEvent } from "../../../helpers/nostr/lists"; +import { listAddEvent, listRemoveEvent } from "../../../helpers/nostr/lists"; import useUserChannelsList from "../../../hooks/use-user-channels-list"; import { usePublishEvent } from "../../../providers/global/publish-provider"; @@ -20,7 +21,7 @@ export default function ChannelJoinButton({ const handleClick = useCallback(async () => { const favList = { - kind: CHANNELS_LIST_KIND, + kind: kinds.PublicChatsList, content: list?.content ?? "", created_at: dayjs().unix(), tags: list?.tags ?? [], diff --git a/src/views/channels/components/channel-metadata-drawer.tsx b/src/views/channels/components/channel-metadata-drawer.tsx index ef9e3046f..f432dff99 100644 --- a/src/views/channels/components/channel-metadata-drawer.tsx +++ b/src/views/channels/components/channel-metadata-drawer.tsx @@ -16,6 +16,7 @@ import { LinkBox, Text, } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; import { NostrEvent } from "../../../types/nostr-event"; import useChannelMetadata from "../../../hooks/use-channel-metadata"; @@ -28,7 +29,6 @@ import UserAvatar from "../../../components/user/user-avatar"; import UserDnsIdentity from "../../../components/user/user-dns-identity"; import ChannelJoinButton from "./channel-join-button"; import { ExternalLinkIcon } from "../../../components/icons"; -import { CHANNELS_LIST_KIND } from "../../../helpers/nostr/lists"; import { useReadRelays } from "../../../hooks/use-client-relays"; import { useAdditionalRelayContext } from "../../../providers/local/additional-relay-context"; @@ -43,7 +43,7 @@ function UserCard({ pubkey }: { pubkey: string }) { } function ChannelMembers({ channel, relays }: { channel: NostrEvent; relays: Iterable }) { const { loader, timeline: userLists } = useTimelineLoader(`${channel.id}-members`, relays, { - kinds: [CHANNELS_LIST_KIND], + kinds: [kinds.PublicChatsList], "#e": [channel.id], }); const callback = useTimelineCurserIntersectionCallback(loader); diff --git a/src/views/communities/components/community-join-button.tsx b/src/views/communities/components/community-join-button.tsx index 3530b28ed..e9b80ff00 100644 --- a/src/views/communities/components/community-join-button.tsx +++ b/src/views/communities/components/community-join-button.tsx @@ -1,12 +1,13 @@ import { useCallback } from "react"; import dayjs from "dayjs"; import { Button, ButtonProps } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; import { DraftNostrEvent, NostrEvent, isDTag } from "../../../types/nostr-event"; import useUserCommunitiesList from "../../../hooks/use-user-communities-list"; import useCurrentAccount from "../../../hooks/use-current-account"; import { getCommunityName } from "../../../helpers/nostr/communities"; -import { COMMUNITIES_LIST_KIND, listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists"; +import { listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists"; import { getEventCoordinate } from "../../../helpers/nostr/event"; import { usePublishEvent } from "../../../providers/global/publish-provider"; @@ -24,7 +25,7 @@ export default function CommunityJoinButton({ const handleClick = useCallback(async () => { const favList = { - kind: COMMUNITIES_LIST_KIND, + kind: kinds.CommunitiesList, content: list?.content ?? "", created_at: dayjs().unix(), tags: list?.tags.filter((t) => !isDTag(t)) ?? [], diff --git a/src/views/communities/index.tsx b/src/views/communities/index.tsx index 75af23dd8..4d7795f61 100644 --- a/src/views/communities/index.tsx +++ b/src/views/communities/index.tsx @@ -28,13 +28,7 @@ import useCurrentAccount from "../../hooks/use-current-account"; import CommunityCard from "./components/community-card"; import CommunityCreateModal, { FormValues } from "./components/community-create-modal"; import { DraftNostrEvent } from "../../types/nostr-event"; -import { - COMMUNITY_APPROVAL_KIND, - COMMUNITY_DEFINITION_KIND, - buildApprovalMap, - getCommunityMods, - getCommunityName, -} from "../../helpers/nostr/communities"; +import { buildApprovalMap, getCommunityMods, getCommunityName } from "../../helpers/nostr/communities"; import { getImageSize } from "../../helpers/image"; import { useReadRelays } from "../../hooks/use-client-relays"; import useTimelineLoader from "../../hooks/use-timeline-loader"; @@ -62,7 +56,7 @@ function CommunitiesHomePage() { const createCommunity = async (values: FormValues) => { const draft: DraftNostrEvent = { - kind: COMMUNITY_DEFINITION_KIND, + kind: kinds.CommunityDefinition, created_at: dayjs().unix(), content: "", tags: [["d", values.name]], @@ -92,7 +86,7 @@ function CommunitiesHomePage() { readRelays, communityCoordinates.length > 0 ? { - kinds: [kinds.ShortTextNote, kinds.Repost, kinds.GenericRepost, COMMUNITY_APPROVAL_KIND], + kinds: [kinds.ShortTextNote, kinds.Repost, kinds.GenericRepost, kinds.CommunityPostApproval], "#a": communityCoordinates.map((p) => createCoordinate(p.kind, p.pubkey, p.identifier)), } : undefined, @@ -113,7 +107,7 @@ function CommunitiesHomePage() { const approvalMap = buildApprovalMap(events, mods); const approved = events - .filter((e) => e.kind !== COMMUNITY_APPROVAL_KIND && (showUnapproved.isOpen ? true : approvalMap.has(e.id))) + .filter((e) => e.kind !== kinds.CommunityPostApproval && (showUnapproved.isOpen ? true : approvalMap.has(e.id))) .map((event) => ({ event, approvals: approvalMap.get(event.id) })) .filter((e) => !muteFilter(e.event)); diff --git a/src/views/community/community-home.tsx b/src/views/community/community-home.tsx index 7832bcae3..b8b070026 100644 --- a/src/views/community/community-home.tsx +++ b/src/views/community/community-home.tsx @@ -5,10 +5,9 @@ import { useObservable } from "applesauce-react/hooks"; import { kinds, nip19 } from "nostr-tools"; import { - getCommunityRelays as getCommunityRelays, + getCommunityRelays, getCommunityImage, getCommunityName, - COMMUNITY_APPROVAL_KIND, getCommunityMods, buildApprovalMap, } from "../../helpers/nostr/communities"; @@ -52,7 +51,7 @@ export default function CommunityHomePage({ community }: { community: NostrEvent const communityRelays = getCommunityRelays(community); const readRelays = useReadRelays(communityRelays); const { loader } = useTimelineLoader(`${getEventUID(community)}-timeline`, readRelays, { - kinds: [kinds.ShortTextNote, kinds.Repost, kinds.GenericRepost, COMMUNITY_APPROVAL_KIND], + kinds: [kinds.ShortTextNote, kinds.Repost, kinds.GenericRepost, kinds.CommunityPostApproval], "#a": [communityCoordinate], }); @@ -60,7 +59,9 @@ export default function CommunityHomePage({ community }: { community: NostrEvent const events = useObservable(loader.timeline) ?? []; const mods = getCommunityMods(community); const approvals = buildApprovalMap(events, mods); - const pending = events.filter((e) => e.kind !== COMMUNITY_APPROVAL_KIND && !approvals.has(e.id) && !muteFilter(e)); + const pending = events.filter( + (e) => e.kind !== kinds.CommunityPostApproval && !approvals.has(e.id) && !muteFilter(e), + ); let active = "newest"; if (location.pathname.endsWith("/newest")) active = "newest"; diff --git a/src/views/community/components/community-edit-modal.tsx b/src/views/community/components/community-edit-modal.tsx index 7ae53d05b..eff481f0f 100644 --- a/src/views/community/components/community-edit-modal.tsx +++ b/src/views/community/components/community-edit-modal.tsx @@ -1,10 +1,10 @@ import { useMemo } from "react"; import { ModalProps } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; import dayjs from "dayjs"; import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event"; import { - COMMUNITY_DEFINITION_KIND, getCommunityDescription, getCommunityImage, getCommunityLinks, @@ -41,7 +41,7 @@ export default function CommunityEditModal({ const updateCommunity = async (values: FormValues) => { const draft: DraftNostrEvent = { - kind: COMMUNITY_DEFINITION_KIND, + kind: kinds.CommunityDefinition, created_at: dayjs().unix(), content: "", tags: [["d", getCommunityName(community)]], diff --git a/src/views/community/components/community-members-modal.tsx b/src/views/community/components/community-members-modal.tsx index 15a0346f3..99dbbca06 100644 --- a/src/views/community/components/community-members-modal.tsx +++ b/src/views/community/components/community-members-modal.tsx @@ -11,13 +11,13 @@ import { ModalProps, SimpleGrid, } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; import { NostrEvent } from "../../../types/nostr-event"; import useTimelineLoader from "../../../hooks/use-timeline-loader"; import { useReadRelays } from "../../../hooks/use-client-relays"; import { getCommunityRelays } from "../../../helpers/nostr/communities"; import { getEventCoordinate } from "../../../helpers/nostr/event"; -import { COMMUNITIES_LIST_KIND } from "../../../helpers/nostr/lists"; import IntersectionObserverProvider from "../../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback"; import TimelineActionAndStatus from "../../../components/timeline/timeline-action-and-status"; @@ -45,7 +45,7 @@ export default function CommunityMembersModal({ const communityCoordinate = getEventCoordinate(community); const readRelays = useReadRelays(getCommunityRelays(community)); const { loader, timeline: lists } = useTimelineLoader(`${communityCoordinate}-members`, readRelays, [ - { "#a": [communityCoordinate], kinds: [COMMUNITIES_LIST_KIND] }, + { "#a": [communityCoordinate], kinds: [kinds.CommunitiesList] }, ]); const callback = useTimelineCurserIntersectionCallback(loader); diff --git a/src/views/community/find-by-name.tsx b/src/views/community/find-by-name.tsx index 0104c5c81..5a9f8f599 100644 --- a/src/views/community/find-by-name.tsx +++ b/src/views/community/find-by-name.tsx @@ -1,9 +1,10 @@ import { useCallback } from "react"; import { Navigate, useParams } from "react-router-dom"; import { Heading, SimpleGrid } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; import { useReadRelays } from "../../hooks/use-client-relays"; -import { COMMUNITY_DEFINITION_KIND, validateCommunity } from "../../helpers/nostr/communities"; +import { validateCommunity } from "../../helpers/nostr/communities"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import { NostrEvent } from "../../types/nostr-event"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; @@ -18,7 +19,7 @@ export default function CommunityFindByNameView() { // if community name is a naddr, redirect const decoded = safeDecode(community); - if (decoded?.type === "naddr" && decoded.data.kind === COMMUNITY_DEFINITION_KIND) { + if (decoded?.type === "naddr" && decoded.data.kind === kinds.CommunityDefinition) { return ; } @@ -29,7 +30,7 @@ export default function CommunityFindByNameView() { const { loader, timeline: communities } = useTimelineLoader( `${community}-find-communities`, readRelays, - community ? { kinds: [COMMUNITY_DEFINITION_KIND], "#d": [community] } : undefined, + community ? { kinds: [kinds.CommunityDefinition], "#d": [community] } : undefined, ); const callback = useTimelineCurserIntersectionCallback(loader); diff --git a/src/views/community/index.tsx b/src/views/community/index.tsx index 55a3612cf..fdd2c3cf7 100644 --- a/src/views/community/index.tsx +++ b/src/views/community/index.tsx @@ -1,7 +1,8 @@ import { useParams } from "react-router-dom"; -import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities"; -import useReplaceableEvent from "../../hooks/use-replaceable-event"; import { Spinner } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; + +import useReplaceableEvent from "../../hooks/use-replaceable-event"; import CommunityHomePage from "./community-home"; import { getPubkeyFromDecodeResult, isHexKey, safeDecode } from "../../helpers/nip19"; @@ -10,12 +11,12 @@ function useCommunityPointer() { const decoded = community ? safeDecode(community) : undefined; if (decoded) { - if (decoded.type === "naddr" && decoded.data.kind === COMMUNITY_DEFINITION_KIND) return decoded.data; + if (decoded.type === "naddr" && decoded.data.kind === kinds.CommunityDefinition) return decoded.data; } else if (community && pubkey) { const hexPubkey = isHexKey(pubkey) ? pubkey : getPubkeyFromDecodeResult(safeDecode(pubkey)); if (!hexPubkey) return; - return { kind: COMMUNITY_DEFINITION_KIND, pubkey: hexPubkey, identifier: community }; + return { kind: kinds.CommunityDefinition, pubkey: hexPubkey, identifier: community }; } } diff --git a/src/views/community/views/newest.tsx b/src/views/community/views/newest.tsx index e606b4ebc..594452119 100644 --- a/src/views/community/views/newest.tsx +++ b/src/views/community/views/newest.tsx @@ -1,13 +1,14 @@ import { useOutletContext } from "react-router-dom"; +import { useObservable } from "applesauce-react/hooks"; +import { kinds } from "nostr-tools"; -import { COMMUNITY_APPROVAL_KIND, buildApprovalMap, getCommunityMods } from "../../../helpers/nostr/communities"; +import { buildApprovalMap, getCommunityMods } from "../../../helpers/nostr/communities"; import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback"; import IntersectionObserverProvider from "../../../providers/local/intersection-observer"; import TimelineActionAndStatus from "../../../components/timeline/timeline-action-and-status"; import useUserMuteFilter from "../../../hooks/use-user-mute-filter"; import ApprovedEvent from "../components/community-approved-post"; import { RouterContext } from "../community-home"; -import { useObservable } from "applesauce-react/hooks"; export default function CommunityNewestView() { const { community, timeline } = useOutletContext(); @@ -18,7 +19,7 @@ export default function CommunityNewestView() { const approvalMap = buildApprovalMap(events, mods); const approved = events - .filter((e) => e.kind !== COMMUNITY_APPROVAL_KIND && approvalMap.has(e.id)) + .filter((e) => e.kind !== kinds.CommunityPostApproval && approvalMap.has(e.id)) .map((event) => ({ event, approvals: approvalMap.get(event.id) })) .filter((e) => !muteFilter(e.event)); diff --git a/src/views/community/views/pending.tsx b/src/views/community/views/pending.tsx index 0510d2cfc..7fbe291d2 100644 --- a/src/views/community/views/pending.tsx +++ b/src/views/community/views/pending.tsx @@ -2,16 +2,12 @@ import { useCallback, useState } from "react"; import { Button, Flex } from "@chakra-ui/react"; import { useOutletContext } from "react-router-dom"; import { useObservable } from "applesauce-react/hooks"; +import { kinds } from "nostr-tools"; import dayjs from "dayjs"; import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event"; import { getEventCoordinate, getEventUID } from "../../../helpers/nostr/event"; -import { - COMMUNITY_APPROVAL_KIND, - buildApprovalMap, - getCommunityMods, - getCommunityRelays, -} from "../../../helpers/nostr/communities"; +import { buildApprovalMap, getCommunityMods, getCommunityRelays } from "../../../helpers/nostr/communities"; import IntersectionObserverProvider from "../../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback"; import TimelineActionAndStatus from "../../../components/timeline/timeline-action-and-status"; @@ -40,7 +36,7 @@ function ModPendingPost({ event, community, approvals }: PendingProps) { setLoading(true); const relay = communityRelays[0]; const draft: DraftNostrEvent = { - kind: COMMUNITY_APPROVAL_KIND, + kind: kinds.CommunityPostApproval, content: JSON.stringify(event), created_at: dayjs().unix(), tags: [ @@ -83,7 +79,9 @@ export default function CommunityPendingView() { const mods = getCommunityMods(community); const approvals = buildApprovalMap(events, mods); - const pending = events.filter((e) => e.kind !== COMMUNITY_APPROVAL_KIND && !approvals.has(e.id) && !muteFilter(e)); + const pending = events.filter( + (e) => e.kind !== kinds.CommunityPostApproval && !approvals.has(e.id) && !muteFilter(e), + ); const callback = useTimelineCurserIntersectionCallback(timeline); diff --git a/src/views/emoji-packs/browse.tsx b/src/views/emoji-packs/browse.tsx index a9eb2c947..10e954aa8 100644 --- a/src/views/emoji-packs/browse.tsx +++ b/src/views/emoji-packs/browse.tsx @@ -1,6 +1,8 @@ import { useCallback } from "react"; import { Flex, SimpleGrid, Switch, useDisclosure } from "@chakra-ui/react"; import { getEventUID } from "applesauce-core/helpers"; +import { getEmojis } from "applesauce-core/helpers/emoji"; +import { kinds } from "nostr-tools"; import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider"; import PeopleListSelection from "../../components/people-list-selection/people-list-selection"; @@ -10,7 +12,6 @@ import { NostrEvent } from "../../types/nostr-event"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import EmojiPackCard from "./components/emoji-pack-card"; -import { EMOJI_PACK_KIND, getEmojis } from "../../helpers/nostr/emoji-packs"; import VerticalPageLayout from "../../components/vertical-page-layout"; function EmojiPacksBrowsePage() { @@ -28,7 +29,7 @@ function EmojiPacksBrowsePage() { const { loader, timeline: packs } = useTimelineLoader( `${listId}-browse-emoji-packs`, readRelays, - filter ? { ...filter, kinds: [EMOJI_PACK_KIND] } : undefined, + filter ? { ...filter, kinds: [kinds.Emojisets] } : undefined, { eventFilter }, ); const callback = useTimelineCurserIntersectionCallback(loader); diff --git a/src/views/emoji-packs/components/create-modal.tsx b/src/views/emoji-packs/components/create-modal.tsx index 16423fc27..25d3889c5 100644 --- a/src/views/emoji-packs/components/create-modal.tsx +++ b/src/views/emoji-packs/components/create-modal.tsx @@ -11,13 +11,12 @@ import { ModalHeader, ModalOverlay, ModalProps, - useToast, } from "@chakra-ui/react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; +import { kinds } from "nostr-tools"; import dayjs from "dayjs"; -import { EMOJI_PACK_KIND } from "../../../helpers/nostr/emoji-packs"; import { DraftNostrEvent } from "../../../types/nostr-event"; import { usePublishEvent } from "../../../providers/global/publish-provider"; import { getSharableEventAddress } from "../../../services/event-relay-hint"; @@ -33,7 +32,7 @@ export default function EmojiPackCreateModal({ onClose, ...props }: Omit { const draft: DraftNostrEvent = { - kind: EMOJI_PACK_KIND, + kind: kinds.Emojisets, created_at: dayjs().unix(), content: "", tags: [["d", values.title]], diff --git a/src/views/emoji-packs/components/emoji-pack-card.tsx b/src/views/emoji-packs/components/emoji-pack-card.tsx index 013f5828d..bb46aeac4 100644 --- a/src/views/emoji-packs/components/emoji-pack-card.tsx +++ b/src/views/emoji-packs/components/emoji-pack-card.tsx @@ -11,12 +11,12 @@ import { Image, Text, } from "@chakra-ui/react"; +import { getEmojis, getPackName } from "applesauce-core/helpers/emoji"; import UserAvatarLink from "../../../components/user/user-avatar-link"; import UserLink from "../../../components/user/user-link"; import { NostrEvent } from "../../../types/nostr-event"; import EmojiPackFavoriteButton from "./emoji-pack-favorite-button"; -import { getEmojis, getPackName } from "../../../helpers/nostr/emoji-packs"; import EmojiPackMenu from "./emoji-pack-menu"; import NoteZapButton from "../../../components/note/note-zap-button"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; diff --git a/src/views/emoji-packs/components/emoji-pack-favorite-button.tsx b/src/views/emoji-packs/components/emoji-pack-favorite-button.tsx index c01518551..6f5566e49 100644 --- a/src/views/emoji-packs/components/emoji-pack-favorite-button.tsx +++ b/src/views/emoji-packs/components/emoji-pack-favorite-button.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { IconButton, IconButtonProps } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; import dayjs from "dayjs"; import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event"; import { StarEmptyIcon, StarFullIcon } from "../../../components/icons"; import { getEventCoordinate } from "../../../helpers/nostr/event"; -import { USER_EMOJI_LIST_KIND } from "../../../helpers/nostr/emoji-packs"; import useFavoriteEmojiPacks from "../../../hooks/use-favorite-emoji-packs"; import { listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists"; import { usePublishEvent } from "../../../providers/global/publish-provider"; @@ -22,7 +22,7 @@ export default function EmojiPackFavoriteButton({ const handleClick = async () => { const prev: DraftNostrEvent = favoritePacks || { - kind: USER_EMOJI_LIST_KIND, + kind: kinds.UserEmojiList, created_at: dayjs().unix(), content: "", tags: [], diff --git a/src/views/emoji-packs/emoji-pack.tsx b/src/views/emoji-packs/emoji-pack.tsx index d9b615552..11e38fc1b 100644 --- a/src/views/emoji-packs/emoji-pack.tsx +++ b/src/views/emoji-packs/emoji-pack.tsx @@ -18,6 +18,8 @@ import { TagLabel, Text, } from "@chakra-ui/react"; +import { getEmojis, getPackName } from "applesauce-core/helpers/emoji"; +import { kinds } from "nostr-tools"; import UserLink from "../../components/user/user-link"; import { ChevronLeftIcon } from "../../components/icons"; @@ -26,7 +28,6 @@ import { useDeleteEventContext } from "../../providers/route/delete-event-provid import useReplaceableEvent from "../../hooks/use-replaceable-event"; import EmojiPackMenu from "./components/emoji-pack-menu"; import EmojiPackFavoriteButton from "./components/emoji-pack-favorite-button"; -import { EMOJI_PACK_KIND, getEmojis, getPackName } from "../../helpers/nostr/emoji-packs"; import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event"; import VerticalPageLayout from "../../components/vertical-page-layout"; import UserAvatarLink from "../../components/user/user-avatar-link"; @@ -121,7 +122,7 @@ function EmojiPackPage({ pack }: { pack: NostrEvent }) { }; const saveEdit = async () => { const draft: DraftNostrEvent = { - kind: EMOJI_PACK_KIND, + kind: kinds.Emojisets, content: pack.content || "", created_at: dayjs().unix(), tags: [...pack.tags.filter((t) => t[0] !== "emoji"), ...draftEmojis.map(({ name, url }) => ["emoji", name, url])], diff --git a/src/views/emoji-packs/index.tsx b/src/views/emoji-packs/index.tsx index e120eb617..85960e84b 100644 --- a/src/views/emoji-packs/index.tsx +++ b/src/views/emoji-packs/index.tsx @@ -1,13 +1,13 @@ import { Button, Flex, Heading, Link, SimpleGrid, useDisclosure } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; -import { useObservable } from "applesauce-react/hooks"; +import { kinds } from "nostr-tools"; import useCurrentAccount from "../../hooks/use-current-account"; import { ExternalLinkIcon } from "../../components/icons"; import { getEventCoordinate, getEventUID } from "../../helpers/nostr/event"; import { useReadRelays } from "../../hooks/use-client-relays"; import useTimelineLoader from "../../hooks/use-timeline-loader"; -import { EMOJI_PACK_KIND, getPackCordsFromFavorites } from "../../helpers/nostr/emoji-packs"; +import { getPackCordsFromFavorites } from "../../helpers/nostr/emoji-packs"; import EmojiPackCard from "./components/emoji-pack-card"; import useFavoriteEmojiPacks from "../../hooks/use-favorite-emoji-packs"; import useReplaceableEvents from "../../hooks/use-replaceable-events"; @@ -25,7 +25,7 @@ function UserEmojiPackMangerPage() { account.pubkey ? { authors: [account.pubkey], - kinds: [EMOJI_PACK_KIND], + kinds: [kinds.Emojisets], } : undefined, ); diff --git a/src/views/launchpad/components/feeds-card.tsx b/src/views/launchpad/components/feeds-card.tsx index ee7333c64..52c88fd6d 100644 --- a/src/views/launchpad/components/feeds-card.tsx +++ b/src/views/launchpad/components/feeds-card.tsx @@ -4,7 +4,6 @@ import { CardBody, CardHeader, CardProps, - Flex, Heading, IconButton, Link, @@ -12,10 +11,11 @@ import { Text, } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; +import { kinds } from "nostr-tools"; -import useUserLists from "../../../hooks/use-user-lists"; +import useUserSets from "../../../hooks/use-user-lists"; import { NostrEvent } from "../../../types/nostr-event"; -import { PEOPLE_LIST_KIND, getListName, getPubkeysFromList } from "../../../helpers/nostr/lists"; +import { getListName, getPubkeysFromList } from "../../../helpers/nostr/lists"; import UserAvatar from "../../../components/user/user-avatar"; import useCurrentAccount from "../../../hooks/use-current-account"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; @@ -49,7 +49,7 @@ function Feed({ list, ...props }: { list: NostrEvent } & Omit) { const account = useCurrentAccount(); const contacts = useUserContactList(account?.pubkey); - const myLists = useUserLists(account?.pubkey).filter((list) => list.kind === PEOPLE_LIST_KIND); + const myLists = useUserSets(account?.pubkey).filter((list) => list.kind === kinds.Followsets); const { lists: favoriteLists } = useFavoriteLists(account?.pubkey); const { recent: recentFeeds, useThing: useFeed } = useRecentIds("feeds", 4); diff --git a/src/views/link/index.tsx b/src/views/link/index.tsx index 0fd1e4ea2..0676e682d 100644 --- a/src/views/link/index.tsx +++ b/src/views/link/index.tsx @@ -2,11 +2,7 @@ import { Alert, AlertIcon, AlertTitle, Spinner } from "@chakra-ui/react"; import { Navigate, useParams } from "react-router-dom"; import { NostrEvent, kinds, nip19 } from "nostr-tools"; -import { STREAM_KIND } from "../../helpers/nostr/stream"; -import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs"; -import { NOTE_LIST_KIND, PEOPLE_LIST_KIND } from "../../helpers/nostr/lists"; import { ErrorBoundary } from "../../components/error-boundary"; -import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities"; import { TORRENT_KIND } from "../../helpers/nostr/torrents"; import { FLARE_VIDEO_KIND } from "../../helpers/nostr/video"; import { WIKI_PAGE_KIND } from "../../helpers/nostr/wiki"; @@ -52,11 +48,12 @@ function RenderRedirect({ event, link }: { event?: NostrEvent; link: string }) { if (k === kinds.ShortTextNote) return ; if (k === TORRENT_KIND) return ; if (k === kinds.LiveEvent) return ; - if (k === EMOJI_PACK_KIND) return ; - if (k === NOTE_LIST_KIND) return ; - if (k === PEOPLE_LIST_KIND) return ; + if (k === kinds.Emojisets) return ; + if (k === kinds.Genericlists) return ; + if (k === kinds.Followsets) return ; + if (k === kinds.Bookmarksets) return ; if (k === kinds.BadgeDefinition) return ; - if (k === COMMUNITY_DEFINITION_KIND) return ; + if (k === kinds.CommunityDefinition) return ; if (k === FLARE_VIDEO_KIND) return ; if (k === kinds.ChannelCreation) return ; if (k === kinds.ShortTextNote) return ; diff --git a/src/views/lists/browse.tsx b/src/views/lists/browse.tsx index 5c962aba3..c16a931e7 100644 --- a/src/views/lists/browse.tsx +++ b/src/views/lists/browse.tsx @@ -1,17 +1,11 @@ import { Flex, Select, SimpleGrid, Switch, useDisclosure } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider"; import PeopleListSelection from "../../components/people-list-selection/people-list-selection"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import { useReadRelays } from "../../hooks/use-client-relays"; -import { - MUTE_LIST_KIND, - NOTE_LIST_KIND, - PEOPLE_LIST_KIND, - getEventPointersFromList, - getListName, - getPubkeysFromList, -} from "../../helpers/nostr/lists"; +import { getEventPointersFromList, getListName, getPubkeysFromList } from "../../helpers/nostr/lists"; import { useCallback, useState } from "react"; import { NostrEvent } from "../../types/nostr-event"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; @@ -24,7 +18,7 @@ function BrowseListPage() { const { filter, listId } = usePeopleListContext(); const showEmpty = useDisclosure(); const showMute = useDisclosure(); - const [listKind, setListKind] = useState(PEOPLE_LIST_KIND); + const [listKind, setListKind] = useState(kinds.Followsets); const eventFilter = useCallback( (event: NostrEvent) => { @@ -33,8 +27,8 @@ function BrowseListPage() { return false; if ( - (!showMute.isOpen && event.kind === PEOPLE_LIST_KIND && getListName(event) === "mute") || - event.kind === MUTE_LIST_KIND + (!showMute.isOpen && event.kind === kinds.Followsets && getListName(event) === "mute") || + event.kind === kinds.Mutelist ) return false; return true; @@ -45,7 +39,7 @@ function BrowseListPage() { const { loader, timeline: lists } = useTimelineLoader( `${listId}-lists`, readRelays, - filter ? { ...filter, kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND] } : undefined, + filter ? { ...filter, kinds: [kinds.Followsets, kinds.Genericlists, kinds.Bookmarksets] } : undefined, { eventFilter }, ); const callback = useTimelineCurserIntersectionCallback(loader); @@ -56,8 +50,9 @@ function BrowseListPage() { Show Empty diff --git a/src/views/lists/components/list-card.tsx b/src/views/lists/components/list-card.tsx index feac85f91..760a5d0f2 100644 --- a/src/views/lists/components/list-card.tsx +++ b/src/views/lists/components/list-card.tsx @@ -30,7 +30,6 @@ import { NostrEvent } from "../../../types/nostr-event"; import useReplaceableEvent from "../../../hooks/use-replaceable-event"; import ListFavoriteButton from "./list-favorite-button"; import ListMenu from "./list-menu"; -import { COMMUNITY_DEFINITION_KIND } from "../../../helpers/nostr/communities"; import { CommunityIcon, NotesIcon } from "../../../components/icons"; import User01 from "../../../components/icons/user-01"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; @@ -46,7 +45,7 @@ export function ListCardContent({ list, ...props }: Omit const people = getPubkeysFromList(list); const notes = getEventPointersFromList(list); const coordinates = getAddressPointersFromList(list); - const communities = coordinates.filter((cord) => cord.kind === COMMUNITY_DEFINITION_KIND); + const communities = coordinates.filter((cord) => cord.kind === kinds.CommunityDefinition); const articles = coordinates.filter((cord) => cord.kind === kinds.LongFormArticle); const references = getReferencesFromList(list); diff --git a/src/views/lists/components/list-favorite-button.tsx b/src/views/lists/components/list-favorite-button.tsx index 5deeedcb2..22748e300 100644 --- a/src/views/lists/components/list-favorite-button.tsx +++ b/src/views/lists/components/list-favorite-button.tsx @@ -1,17 +1,13 @@ import { useState } from "react"; import { IconButton, IconButtonProps } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; import dayjs from "dayjs"; import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event"; import { StarEmptyIcon, StarFullIcon } from "../../../components/icons"; import { getEventCoordinate } from "../../../helpers/nostr/event"; import useFavoriteLists, { FAVORITE_LISTS_IDENTIFIER } from "../../../hooks/use-favorite-lists"; -import { - NOTE_LIST_KIND, - isSpecialListKind, - listAddCoordinate, - listRemoveCoordinate, -} from "../../../helpers/nostr/lists"; +import { isSpecialListKind, listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists"; import { usePublishEvent } from "../../../providers/global/publish-provider"; export default function ListFavoriteButton({ @@ -26,8 +22,8 @@ export default function ListFavoriteButton({ if (isSpecialListKind(list.kind)) return null; - // NOTE: dont show favorite button for note lists - if (list.kind === NOTE_LIST_KIND) return null; + // NOTE: don't show favorite button for note lists + if (list.kind === kinds.Genericlists) return null; const handleClick = async () => { const prev: DraftNostrEvent = favoriteList || { diff --git a/src/views/lists/components/list-feed-button.tsx b/src/views/lists/components/list-feed-button.tsx index db7201375..44bfb0399 100644 --- a/src/views/lists/components/list-feed-button.tsx +++ b/src/views/lists/components/list-feed-button.tsx @@ -4,10 +4,9 @@ import { kinds } from "nostr-tools"; import { NostrEvent } from "../../../types/nostr-event"; import { getEventCoordinate } from "../../../helpers/nostr/event"; -import { PEOPLE_LIST_KIND } from "../../../helpers/nostr/lists"; export default function ListFeedButton({ list, ...props }: { list: NostrEvent } & Omit) { - const shouldShowFeedButton = list.kind === PEOPLE_LIST_KIND || list.kind === kinds.Contacts; + const shouldShowFeedButton = list.kind === kinds.Followsets || list.kind === kinds.Contacts; if (!shouldShowFeedButton) return null; diff --git a/src/views/lists/components/list-menu.tsx b/src/views/lists/components/list-menu.tsx index 30f40f644..ba2dcf9a8 100644 --- a/src/views/lists/components/list-menu.tsx +++ b/src/views/lists/components/list-menu.tsx @@ -1,34 +1,20 @@ -import { Image, MenuItem } from "@chakra-ui/react"; - -import { NostrEvent, isPTag } from "../../../types/nostr-event"; +import { NostrEvent } from "../../../types/nostr-event"; import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code"; import { isSpecialListKind } from "../../../helpers/nostr/lists"; import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu-item"; -import useShareableEventAddress from "../../../hooks/use-shareable-event-address"; export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit) { - const address = useShareableEventAddress(list); const isSpecial = isSpecialListKind(list.kind); - const hasPeople = list.tags.some(isPTag); - return ( <> {!isSpecial && } - {hasPeople && ( - } - onClick={() => window.open(`https://www.makeprisms.com/create/${address}`, "_blank")} - > - Create $prism - - )} diff --git a/src/views/lists/components/new-list-modal.tsx b/src/views/lists/components/new-set-modal.tsx similarity index 80% rename from src/views/lists/components/new-list-modal.tsx rename to src/views/lists/components/new-set-modal.tsx index 177e0933c..4e748edd8 100644 --- a/src/views/lists/components/new-list-modal.tsx +++ b/src/views/lists/components/new-set-modal.tsx @@ -14,29 +14,30 @@ import { ModalProps, Select, } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; import dayjs from "dayjs"; -import { NOTE_LIST_KIND, PEOPLE_LIST_KIND } from "../../../helpers/nostr/lists"; import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event"; import { usePublishEvent } from "../../../providers/global/publish-provider"; +import { nanoid } from "nanoid"; -export type NewListModalProps = Omit & { +export type NewSetModalProps = Omit & { onCreated?: (list: NostrEvent) => void; initKind?: number; allowSelectKind?: boolean; }; -export default function NewListModal({ +export default function NewSetModal({ onClose, onCreated, initKind, allowSelectKind = true, ...props -}: NewListModalProps) { +}: NewSetModalProps) { const publish = usePublishEvent(); const { handleSubmit, register, formState } = useForm({ defaultValues: { - kind: initKind || PEOPLE_LIST_KIND, + kind: initKind || kinds.Followsets, name: "", }, }); @@ -45,7 +46,10 @@ export default function NewListModal({ const draft: DraftNostrEvent = { content: "", created_at: dayjs().unix(), - tags: [["d", values.name]], + tags: [ + ["d", nanoid()], + ["title", values.name], + ], kind: values.kind, }; const pub = await publish("Create list", draft); @@ -66,8 +70,9 @@ export default function NewListModal({ List kind )} diff --git a/src/views/lists/index.tsx b/src/views/lists/index.tsx index b40bb0213..08470570c 100644 --- a/src/views/lists/index.tsx +++ b/src/views/lists/index.tsx @@ -1,34 +1,27 @@ -import { Button, Flex, Heading, Image, Link, SimpleGrid, Spacer, useDisclosure } from "@chakra-ui/react"; +import { Button, Flex, Heading, SimpleGrid, Spacer, useDisclosure } from "@chakra-ui/react"; import { useNavigate, Link as RouterLink, Navigate } from "react-router-dom"; +import { getEventUID } from "applesauce-core/helpers"; import { kinds } from "nostr-tools"; import useCurrentAccount from "../../hooks/use-current-account"; -import { ExternalLinkIcon, PlusCircleIcon } from "../../components/icons"; import ListCard from "./components/list-card"; -import { getEventUID } from "../../helpers/nostr/event"; -import useUserLists from "../../hooks/use-user-lists"; -import NewListModal from "./components/new-list-modal"; -import { - BOOKMARK_LIST_KIND, - COMMUNITIES_LIST_KIND, - MUTE_LIST_KIND, - NOTE_LIST_KIND, - PEOPLE_LIST_KIND, - PIN_LIST_KIND, -} from "../../helpers/nostr/lists"; +import useUserSets from "../../hooks/use-user-lists"; +import NewSetModal from "./components/new-set-modal"; import useFavoriteLists from "../../hooks/use-favorite-lists"; import VerticalPageLayout from "../../components/vertical-page-layout"; import { getSharableEventAddress } from "../../services/event-relay-hint"; +import Plus from "../../components/icons/plus"; function ListsHomePage() { const account = useCurrentAccount()!; - const lists = useUserLists(account.pubkey); + const sets = useUserSets(account.pubkey, undefined, true); const { lists: favoriteLists } = useFavoriteLists(); const newList = useDisclosure(); const navigate = useNavigate(); - const peopleLists = lists.filter((event) => event.kind === PEOPLE_LIST_KIND); - const noteLists = lists.filter((event) => event.kind === NOTE_LIST_KIND); + const followSets = sets.filter((event) => event.kind === kinds.Followsets); + const genericSets = sets.filter((event) => event.kind === kinds.Genericlists); + const bookmarkSets = sets.filter((event) => event.kind === kinds.Bookmarksets); return ( @@ -37,16 +30,7 @@ function ListsHomePage() { Browse Lists - - @@ -56,30 +40,42 @@ function ListsHomePage() { - - - - + + + + - {peopleLists.length > 0 && ( + {followSets.length > 0 && ( <> People lists - {peopleLists.map((event) => ( + {followSets.map((event) => ( ))} )} - {noteLists.length > 0 && ( + {genericSets.length > 0 && ( + <> + + Generic lists + + + {genericSets.map((event) => ( + + ))} + + + )} + {bookmarkSets.length > 0 && ( <> Bookmark lists - {noteLists.map((event) => ( + {genericSets.map((event) => ( ))} @@ -99,7 +95,7 @@ function ListsHomePage() { )} {newList.isOpen && ( - navigate(`/lists/${getSharableEventAddress(list)}`)} diff --git a/src/views/lists/list/index.tsx b/src/views/lists/list/index.tsx index 8bbb1d0a0..c78ce78cc 100644 --- a/src/views/lists/list/index.tsx +++ b/src/views/lists/list/index.tsx @@ -25,7 +25,6 @@ import ListMenu from "../components/list-menu"; import ListFavoriteButton from "../components/list-favorite-button"; import ListFeedButton from "../components/list-feed-button"; import VerticalPageLayout from "../../../components/vertical-page-layout"; -import { COMMUNITY_DEFINITION_KIND } from "../../../helpers/nostr/communities"; import { EmbedEvent, EmbedEventPointer } from "../../../components/embed-event"; import useSingleEvent from "../../../hooks/use-single-event"; import UserAvatarLink from "../../../components/user/user-avatar-link"; @@ -48,7 +47,7 @@ function ListPage({ list }: { list: NostrEvent }) { const people = getPubkeysFromList(list); const notes = getEventPointersFromList(list); const coordinates = getAddressPointersFromList(list); - const communities = coordinates.filter((cord) => cord.kind === COMMUNITY_DEFINITION_KIND); + const communities = coordinates.filter((cord) => cord.kind === kinds.CommunityDefinition); const articles = coordinates.filter((cord) => cord.kind === kinds.LongFormArticle); const references = getReferencesFromList(list); diff --git a/src/views/tools/unknown-event-feed.tsx b/src/views/tools/unknown-event-feed.tsx index abf3acd95..e10987e95 100644 --- a/src/views/tools/unknown-event-feed.tsx +++ b/src/views/tools/unknown-event-feed.tsx @@ -14,14 +14,6 @@ import { NostrEvent } from "../../types/nostr-event"; import { ChevronLeftIcon } from "../../components/icons"; import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter"; import { EmbedEvent } from "../../components/embed-event"; -import { - BOOKMARK_LIST_KIND, - BOOKMARK_LIST_SET_KIND, - COMMUNITIES_LIST_KIND, - MUTE_LIST_KIND, - PEOPLE_LIST_KIND, - PIN_LIST_KIND, -} from "../../helpers/nostr/lists"; import useEventIntersectionRef from "../../hooks/use-event-intersection-ref"; const UnknownEvent = memo(({ event }: { event: NostrEvent }) => { @@ -46,15 +38,15 @@ const commonTimelineKinds = [ kinds.Contacts, kinds.Metadata, kinds.EncryptedDirectMessage, - MUTE_LIST_KIND, + kinds.Mutelist, kinds.LiveChatMessage, kinds.EventDeletion, kinds.CommunityPostApproval, - BOOKMARK_LIST_KIND, - BOOKMARK_LIST_SET_KIND, - PEOPLE_LIST_KIND, - PIN_LIST_KIND, - COMMUNITIES_LIST_KIND, + kinds.BookmarkList, + kinds.Bookmarksets, + kinds.Followsets, + kinds.Pinlist, + kinds.CommunitiesList, kinds.ZapGoal, ]; diff --git a/src/views/user/about/user-recent-events.tsx b/src/views/user/about/user-recent-events.tsx index ef1191604..6d1fcdad8 100644 --- a/src/views/user/about/user-recent-events.tsx +++ b/src/views/user/about/user-recent-events.tsx @@ -2,7 +2,17 @@ import { Badge, Button, ButtonProps, ComponentWithAs, Flex, IconProps, useDisclo import { Filter, kinds, nip19, NostrEvent } from "nostr-tools"; import { Link as RouteLink, To } from "react-router-dom"; -import { ArticleIcon, DirectMessagesIcon, ListsIcon, NotesIcon, RepostIcon } from "../../../components/icons"; +import { + ArticleIcon, + BookmarkIcon, + ChannelsIcon, + CommunityIcon, + DirectMessagesIcon, + ListsIcon, + NotesIcon, + RelayIcon, + RepostIcon, +} from "../../../components/icons"; import AnnotationQuestion from "../../../components/icons/annotation-question"; import { getSharableEventAddress } from "../../../services/event-relay-hint"; import { npubEncode } from "nostr-tools/nip19"; @@ -10,6 +20,7 @@ import useTimelineLoader from "../../../hooks/use-timeline-loader"; import { useUserOutbox } from "../../../hooks/use-user-mailboxes"; import { useReadRelays } from "../../../hooks/use-client-relays"; import AlertTriangle from "../../../components/icons/alert-triangle"; +import MessageSquare02 from "../../../components/icons/message-square-02"; type KnownKind = { kind: number; @@ -70,15 +81,29 @@ const KnownKinds: KnownKind[] = [ link: (_e, p) => `/u/${nip19.npubEncode(p)}/dms`, }, - { kind: kinds.Followsets, name: "Lists", icon: ListsIcon, link: (_e, p) => `/u/${npubEncode(p)}/lists` }, + { + kind: kinds.PublicChatsList, + icon: ChannelsIcon, + name: "Public Chats", + link: (_e, p) => `/u/${npubEncode(p)}/lists`, + }, + + { kind: kinds.Followsets, name: "People Lists", icon: ListsIcon, link: (_e, p) => `/u/${npubEncode(p)}/lists` }, + { kind: kinds.Genericlists, icon: ListsIcon, name: "Generic Lists", link: (_e, p) => `/u/${npubEncode(p)}/lists` }, + { kind: kinds.Relaysets, icon: RelayIcon, name: "Relay Sets" }, + { kind: kinds.Bookmarksets, icon: BookmarkIcon, name: "Bookmarks", link: (_e, p) => `/u/${npubEncode(p)}/lists` }, { kind: kinds.Report, name: "Report", icon: AlertTriangle, link: (_e, p) => `/u/${npubEncode(p)}/reports` }, { kind: kinds.Handlerinformation, name: "Application" }, { kind: kinds.Handlerrecommendation, name: "App recommendation" }, + { kind: kinds.CommunityDefinition, icon: CommunityIcon, name: "Communities" }, + { kind: kinds.BadgeAward, name: "Badge Award" }, + { kind: kinds.LiveChatMessage, icon: MessageSquare02, name: "Stream Chat" }, + // common kinds { kind: kinds.Metadata, hidden: true }, { kind: kinds.Contacts, hidden: true }, diff --git a/src/views/user/emoji-packs.tsx b/src/views/user/emoji-packs.tsx index 03234b248..7851d1357 100644 --- a/src/views/user/emoji-packs.tsx +++ b/src/views/user/emoji-packs.tsx @@ -1,6 +1,6 @@ import { useOutletContext } from "react-router-dom"; import { Heading, SimpleGrid } from "@chakra-ui/react"; -import { useObservable } from "applesauce-react/hooks"; +import { kinds } from "nostr-tools"; import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context"; import useTimelineLoader from "../../hooks/use-timeline-loader"; @@ -8,7 +8,7 @@ import { getEventUID } from "../../helpers/nostr/event"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import EmojiPackCard from "../emoji-packs/components/emoji-pack-card"; -import { EMOJI_PACK_KIND, getPackCordsFromFavorites } from "../../helpers/nostr/emoji-packs"; +import { getPackCordsFromFavorites } from "../../helpers/nostr/emoji-packs"; import useFavoriteEmojiPacks from "../../hooks/use-favorite-emoji-packs"; import useReplaceableEvents from "../../hooks/use-replaceable-events"; import VerticalPageLayout from "../../components/vertical-page-layout"; @@ -19,7 +19,7 @@ export default function UserEmojiPacksTab() { const { loader, timeline: packs } = useTimelineLoader(pubkey + "-emoji-packs", readRelays, { authors: [pubkey], - kinds: [EMOJI_PACK_KIND], + kinds: [kinds.Emojisets], }); const favoritePacks = useFavoriteEmojiPacks(pubkey); diff --git a/src/views/user/lists.tsx b/src/views/user/lists.tsx index 12ad634e3..469e38cb3 100644 --- a/src/views/user/lists.tsx +++ b/src/views/user/lists.tsx @@ -1,104 +1,73 @@ -import { useCallback } from "react"; import { useOutletContext } from "react-router-dom"; import { Heading, SimpleGrid } from "@chakra-ui/react"; +import { getEventUID } from "applesauce-core/helpers"; import { kinds } from "nostr-tools"; -import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context"; -import useTimelineLoader from "../../hooks/use-timeline-loader"; -import { - BOOKMARK_LIST_KIND, - MUTE_LIST_KIND, - NOTE_LIST_KIND, - PEOPLE_LIST_KIND, - PIN_LIST_KIND, - isJunkList, -} from "../../helpers/nostr/lists"; -import { getEventUID } from "../../helpers/nostr/event"; +import { isJunkList } from "../../helpers/nostr/lists"; import ListCard from "../lists/components/list-card"; -import IntersectionObserverProvider from "../../providers/local/intersection-observer"; -import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import VerticalPageLayout from "../../components/vertical-page-layout"; -import { NostrEvent } from "../../types/nostr-event"; -import UserName from "../../components/user/user-name"; +import useUserSets from "../../hooks/use-user-lists"; export default function UserListsTab() { const { pubkey } = useOutletContext() as { pubkey: string }; - const readRelays = useAdditionalRelayContext(); + const sets = useUserSets(pubkey).filter((e) => !isJunkList(e)); - const eventFilter = useCallback((event: NostrEvent) => { - return !isJunkList(event); - }, []); - const { loader, timeline: lists } = useTimelineLoader( - pubkey + "-lists", - readRelays, - [ - { - authors: [pubkey], - kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND], - }, - { - "#p": [pubkey], - kinds: [PEOPLE_LIST_KIND], - }, - ], - { eventFilter }, - ); - const callback = useTimelineCurserIntersectionCallback(loader); - - const peopleLists = lists.filter((event) => event.pubkey === pubkey && event.kind === PEOPLE_LIST_KIND); - const noteLists = lists.filter((event) => event.pubkey === pubkey && event.kind === NOTE_LIST_KIND); - const otherLists = lists.filter((event) => event.pubkey !== pubkey && event.kind === PEOPLE_LIST_KIND); + const followSets = sets.filter((event) => event.pubkey === pubkey && event.kind === kinds.Followsets); + const genericSets = sets.filter((event) => event.pubkey === pubkey && event.kind === kinds.Genericlists); + const bookmarkSets = sets.filter((event) => event.pubkey === pubkey && event.kind === kinds.Bookmarksets); return ( - - - Special lists - - - - - - - + + Special lists + + + + + + + + + - {peopleLists.length > 0 && ( - <> - - People lists - - - {peopleLists.map((event) => ( - - ))} - - - )} + {followSets.length > 0 && ( + <> + + People lists + + + {followSets.map((set) => ( + + ))} + + + )} - {noteLists.length > 0 && ( - <> - - Bookmark lists - - - {noteLists.map((event) => ( - - ))} - - - )} - + {genericSets.length > 0 && ( + <> + + Generic lists + + + {genericSets.map((set) => ( + + ))} + + + )} - - - Lists is in - - - {otherLists.map((event) => ( - - ))} - - + {bookmarkSets.length > 0 && ( + <> + + Bookmark sets + + + {bookmarkSets.map((set) => ( + + ))} + + + )} ); } diff --git a/src/views/user/muted-by.tsx b/src/views/user/muted-by.tsx index 736247b70..9a9476cce 100644 --- a/src/views/user/muted-by.tsx +++ b/src/views/user/muted-by.tsx @@ -1,12 +1,13 @@ import { memo, useMemo } from "react"; import { Flex, Heading, Link, SimpleGrid } from "@chakra-ui/react"; import { Link as RouterLink, useOutletContext } from "react-router-dom"; +import { kinds } from "nostr-tools"; import UserAvatarLink from "../../components/user/user-avatar-link"; import UserLink from "../../components/user/user-link"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import { useReadRelays } from "../../hooks/use-client-relays"; -import { MUTE_LIST_KIND, PEOPLE_LIST_KIND, getListName, getPubkeysFromList } from "../../helpers/nostr/lists"; +import { getListName, getPubkeysFromList } from "../../helpers/nostr/lists"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import VerticalPageLayout from "../../components/vertical-page-layout"; @@ -43,8 +44,8 @@ export default function UserMutedByTab() { const readRelays = useReadRelays(); const { loader, timeline: lists } = useTimelineLoader(`${pubkey}-muted-by`, readRelays, [ - { kinds: [MUTE_LIST_KIND], "#p": [pubkey] }, - { kinds: [PEOPLE_LIST_KIND], "#d": ["mute"], "#p": [pubkey] }, + { kinds: [kinds.Mutelist], "#p": [pubkey] }, + { kinds: [kinds.Followsets], "#d": ["mute"], "#p": [pubkey] }, ]); const pubkeys = useMemo(() => {