Improve list background loading

Fix delete events not getting published to outbox
This commit is contained in:
hzrd149 2024-11-25 15:53:51 -06:00
parent 30a3849ef3
commit cab89b637f
64 changed files with 517 additions and 495 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Fix delete events not getting published to outbox

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Improve list background loading

View File

@ -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",

16
pnpm-lock.yaml generated
View File

@ -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

View File

@ -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;

View File

@ -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) : [],

View File

@ -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";

View File

@ -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<CardProps, "children"> & { list: NostrEvent }) {
export default function EmbeddedSetOrList({ list, ...props }: Omit<CardProps, "children"> & { list: NostrEvent }) {
const link = isSpecialListKind(list.kind) ? createCoordinate(list.kind, list.pubkey) : getSharableEventAddress(list);
const description = getListDescription(list);

View File

@ -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 <EmbeddedDM dm={event} {...cardProps} />;
case kinds.LiveEvent:
return <EmbeddedStream event={event} {...cardProps} />;
case GOAL_KIND:
case kinds.ZapGoal:
return <EmbeddedGoal goal={event} {...cardProps} {...goalProps} />;
case EMOJI_PACK_KIND:
case kinds.Emojisets:
return <EmbeddedEmojiPack pack={event} {...cardProps} />;
case PEOPLE_LIST_KIND:
case NOTE_LIST_KIND:
case BOOKMARK_LIST_KIND:
case COMMUNITIES_LIST_KIND:
case CHANNELS_LIST_KIND:
return <EmbeddedList list={event} {...cardProps} />;
case kinds.LongFormArticle:
return <EmbeddedArticle article={event} {...cardProps} />;
case kinds.BadgeDefinition:
return <EmbeddedBadge badge={event} {...cardProps} />;
case kinds.LiveChatMessage:
return <EmbeddedStreamMessage message={event} {...cardProps} />;
case COMMUNITY_DEFINITION_KIND:
case kinds.CommunityDefinition:
return <EmbeddedCommunity community={event} {...cardProps} />;
case STEMSTR_TRACK_KIND:
return <EmbeddedStemstrTrack track={event} {...cardProps} />;
@ -103,6 +87,9 @@ export function EmbedEvent({
return <EmbeddedZapRecept zap={event} {...cardProps} />;
}
if (SET_KINDS.includes(event.kind) || LIST_KINDS.includes(event.kind))
return <EmbeddedSetOrList list={event} {...cardProps} />;
return <EmbeddedUnknown event={event} {...cardProps} />;
};

View File

@ -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<IconButtonProps, "icon">) {
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({
<Menu isLazy closeOnSelect={false}>
<MenuButton
as={IconButton}
icon={inLists.length > 0 || isBookmarked ? <BookmarkedIcon /> : <BookmarkIcon />}
icon={inSets.length > 0 || isBookmarked ? <BookmarkedIcon /> : <BookmarkIcon />}
isDisabled={account?.readonly ?? true}
{...props}
/>
@ -80,37 +79,37 @@ export default function BookmarkEventButton({
Bookmark
</MenuItem>
<MenuDivider />
{lists.length > 0 && (
{bookmarkSets.length > 0 && (
<MenuOptionGroup
type="checkbox"
value={inLists.map((list) => getEventCoordinate(list))}
value={inSets.map((set) => getEventCoordinate(set))}
onChange={handleChange}
>
{lists.map((list) => (
{bookmarkSets.map((set) => (
<MenuItemOption
key={getEventCoordinate(list)}
value={getEventCoordinate(list)}
key={getEventCoordinate(set)}
value={getEventCoordinate(set)}
isDisabled={account?.readonly || isLoading}
isTruncated
maxW="90vw"
>
{getListName(list)}
{getListName(set)}
</MenuItemOption>
))}
</MenuOptionGroup>
)}
<MenuDivider />
<MenuItem icon={<PlusCircleIcon />} onClick={newListModal.onOpen}>
<MenuItem icon={<PlusCircleIcon />} onClick={newSetModal.onOpen}>
New list
</MenuItem>
</MenuList>
</Menu>
{newListModal.isOpen && (
<NewListModal
onClose={newListModal.onClose}
{newSetModal.isOpen && (
<NewSetModal
onClose={newSetModal.onClose}
isOpen
onCreated={newListModal.onClose}
initKind={NOTE_LIST_KIND}
onCreated={newSetModal.onClose}
initKind={kinds.Bookmarksets}
allowSelectKind={false}
/>
)}

View File

@ -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<ButtonProps, "children">) {
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({
</Heading>
<SimpleGrid columns={2} spacing="2">
{lists
.filter((l) => l.kind === PEOPLE_LIST_KIND)
.filter((l) => l.kind === kinds.Followsets)
.map((list) => (
<ListCard key={getEventUID(list)} list={list} onClick={() => selectList(list)} />
))}

View File

@ -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";

View File

@ -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
</MenuItem>
{newListModal.isOpen && <NewListModal onClose={newListModal.onClose} isOpen onCreated={newListModal.onClose} />}
{newListModal.isOpen && <NewSetModal onClose={newListModal.onClose} isOpen onCreated={newListModal.onClose} />}
</>
);
}

View File

@ -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<NostrEvent>, mods: string[]) {
const approvals = new Map<string, NostrEvent[]>();
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<NostrEvent>, 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;
}

View File

@ -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]);
}

View File

@ -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 {

View File

@ -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) => {

View File

@ -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] });
}

View File

@ -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(),

View File

@ -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,
);

View File

@ -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]);

View File

@ -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) : [];

View File

@ -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 };

View File

@ -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<string>) {
export default function useUserSets(pubkey?: string, additionalRelays?: Iterable<string>, 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;
}

View File

@ -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<string>,
opts: RequestOptions = {},
) {
return useReplaceableEvent(pubkey && { kind: MUTE_LIST_KIND, pubkey }, additionalRelays, opts);
return useReplaceableEvent(pubkey && { kind: kinds.Mutelist, pubkey }, additionalRelays, opts);
}

View File

@ -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,
);

View File

@ -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) : [];

View File

@ -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<Event>();
const [defer, setDefer] = useState<Deferred<void>>();
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();

View File

@ -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: {

View File

@ -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<RelayInformationDocument>,
);
sanitizeInfo(infoDoc);
memoryCache.set(relay, infoDoc);
await db.put("relayInfo", infoDoc, relay);
return infoDoc;
}
const memoryCache = new Map<string, RelayInformationDocument>();
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<string, ReturnType<typeof getInfo> | 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;

View File

@ -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<string>, 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();
};
}

58
src/services/user-sets.ts Normal file
View File

@ -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<string, boolean>();
loaders = new SuperMap<AbstractRelay, BatchKindPubkeyLoader>((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<string | URL | AbstractRelay>, 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;

View File

@ -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 ?? [],

View File

@ -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<string> }) {
const { loader, timeline: userLists } = useTimelineLoader(`${channel.id}-members`, relays, {
kinds: [CHANNELS_LIST_KIND],
kinds: [kinds.PublicChatsList],
"#e": [channel.id],
});
const callback = useTimelineCurserIntersectionCallback(loader);

View File

@ -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)) ?? [],

View File

@ -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));

View File

@ -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";

View File

@ -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)]],

View File

@ -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);

View File

@ -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 <Navigate to={`/c/${decoded.data.identifier}/${decoded.data.pubkey}`} replace />;
}
@ -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);

View File

@ -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 };
}
}

View File

@ -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<RouterContext>();
@ -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));

View File

@ -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);

View File

@ -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);

View File

@ -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<ModalPr
const submit = handleSubmit(async (values) => {
const draft: DraftNostrEvent = {
kind: EMOJI_PACK_KIND,
kind: kinds.Emojisets,
created_at: dayjs().unix(),
content: "",
tags: [["d", values.title]],

View File

@ -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";

View File

@ -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: [],

View File

@ -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])],

View File

@ -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,
);

View File

@ -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<CardProps, "childr
export default function FeedsCard({ ...props }: Omit<CardProps, "children">) {
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);

View File

@ -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 <Navigate to={`/n/${link}`} replace />;
if (k === TORRENT_KIND) return <Navigate to={`/torrents/${link}`} replace />;
if (k === kinds.LiveEvent) return <Navigate to={`/streams/${link}`} replace />;
if (k === EMOJI_PACK_KIND) return <Navigate to={`/emojis/${link}`} replace />;
if (k === NOTE_LIST_KIND) return <Navigate to={`/lists/${link}`} replace />;
if (k === PEOPLE_LIST_KIND) return <Navigate to={`/lists/${link}`} replace />;
if (k === kinds.Emojisets) return <Navigate to={`/emojis/${link}`} replace />;
if (k === kinds.Genericlists) return <Navigate to={`/lists/${link}`} replace />;
if (k === kinds.Followsets) return <Navigate to={`/lists/${link}`} replace />;
if (k === kinds.Bookmarksets) return <Navigate to={`/lists/${link}`} replace />;
if (k === kinds.BadgeDefinition) return <Navigate to={`/badges/${link}`} replace />;
if (k === COMMUNITY_DEFINITION_KIND) return <Navigate to={`/c/${link}`} replace />;
if (k === kinds.CommunityDefinition) return <Navigate to={`/c/${link}`} replace />;
if (k === FLARE_VIDEO_KIND) return <Navigate to={`/videos/${link}`} replace />;
if (k === kinds.ChannelCreation) return <Navigate to={`/channels/${link}`} replace />;
if (k === kinds.ShortTextNote) return <Navigate to={`/n/${link}`} replace />;

View File

@ -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() {
<Flex gap="2" alignItems="center" wrap="wrap">
<PeopleListSelection />
<Select w="sm" value={listKind} onChange={(e) => setListKind(parseInt(e.target.value))}>
<option value={PEOPLE_LIST_KIND}>People List</option>
<option value={NOTE_LIST_KIND}>Note List</option>
<option value={kinds.Followsets}>People list</option>
<option value={kinds.Genericlists}>Note list</option>
<option value={kinds.Bookmarksets}>Bookmark sets</option>
</Select>
<Switch isChecked={showEmpty.isOpen} onChange={showEmpty.onToggle} whiteSpace="pre">
Show Empty

View File

@ -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<CardProps, "children">
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);

View File

@ -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 || {

View File

@ -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<ButtonProps, "children">) {
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;

View File

@ -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<MenuIconButtonProps, "children">) {
const address = useShareableEventAddress(list);
const isSpecial = isSpecialListKind(list.kind);
const hasPeople = list.tags.some(isPTag);
return (
<>
<DotsMenuButton {...props}>
<OpenInAppMenuItem event={list} />
<CopyEmbedCodeMenuItem event={list} />
{!isSpecial && <DeleteEventMenuItem event={list} label="Delete List" />}
{hasPeople && (
<MenuItem
icon={<Image w="4" h="4" src="https://framerusercontent.com/images/3S3Pyvkh2tEvvKyX47QrUq7XQLk.png" />}
onClick={() => window.open(`https://www.makeprisms.com/create/${address}`, "_blank")}
>
Create $prism
</MenuItem>
)}
<DebugEventMenuItem event={list} />
</DotsMenuButton>
</>

View File

@ -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<ModalProps, "children"> & {
export type NewSetModalProps = Omit<ModalProps, "children"> & {
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({
<FormControl isRequired>
<FormLabel>List kind</FormLabel>
<Select {...register("kind", { valueAsNumber: true, required: true })}>
<option value={PEOPLE_LIST_KIND}>People List</option>
<option value={NOTE_LIST_KIND}>Note List</option>
<option value={kinds.Followsets}>People List</option>
<option value={kinds.Genericlists}>Generic List</option>
<option value={kinds.Bookmarksets}>Bookmark set</option>
</Select>
</FormControl>
)}

View File

@ -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 (
<VerticalPageLayout>
@ -37,16 +30,7 @@ function ListsHomePage() {
Browse Lists
</Button>
<Spacer />
<Button
as={Link}
href="https://listr.lol/"
isExternal
rightIcon={<ExternalLinkIcon />}
leftIcon={<Image src="https://listr.lol/favicon.ico" w="1.2em" />}
>
Listr
</Button>
<Button leftIcon={<PlusCircleIcon />} onClick={newList.onOpen} colorScheme="primary">
<Button leftIcon={<Plus boxSize={5} />} onClick={newList.onOpen} colorScheme="primary">
New List
</Button>
</Flex>
@ -56,30 +40,42 @@ function ListsHomePage() {
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
<ListCard cord={`${kinds.Contacts}:${account.pubkey}`} hideCreator />
<ListCard cord={`${MUTE_LIST_KIND}:${account.pubkey}`} hideCreator />
<ListCard cord={`${PIN_LIST_KIND}:${account.pubkey}`} hideCreator />
<ListCard cord={`${COMMUNITIES_LIST_KIND}:${account.pubkey}`} hideCreator />
<ListCard cord={`${BOOKMARK_LIST_KIND}:${account.pubkey}`} hideCreator />
<ListCard cord={`${kinds.Mutelist}:${account.pubkey}`} hideCreator />
<ListCard cord={`${kinds.Pinlist}:${account.pubkey}`} hideCreator />
<ListCard cord={`${kinds.CommunitiesList}:${account.pubkey}`} hideCreator />
<ListCard cord={`${kinds.BookmarkList}:${account.pubkey}`} hideCreator />
</SimpleGrid>
{peopleLists.length > 0 && (
{followSets.length > 0 && (
<>
<Heading size="lg" mt="2">
People lists
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
{peopleLists.map((event) => (
{followSets.map((event) => (
<ListCard key={getEventUID(event)} list={event} hideCreator />
))}
</SimpleGrid>
</>
)}
{noteLists.length > 0 && (
{genericSets.length > 0 && (
<>
<Heading size="lg" mt="2">
Generic lists
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
{genericSets.map((event) => (
<ListCard key={getEventUID(event)} list={event} hideCreator />
))}
</SimpleGrid>
</>
)}
{bookmarkSets.length > 0 && (
<>
<Heading size="lg" mt="2">
Bookmark lists
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
{noteLists.map((event) => (
{genericSets.map((event) => (
<ListCard key={getEventUID(event)} list={event} hideCreator />
))}
</SimpleGrid>
@ -99,7 +95,7 @@ function ListsHomePage() {
)}
{newList.isOpen && (
<NewListModal
<NewSetModal
isOpen
onClose={newList.onClose}
onCreated={(list) => navigate(`/lists/${getSharableEventAddress(list)}`)}

View File

@ -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);

View File

@ -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,
];

View File

@ -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 },

View File

@ -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);

View File

@ -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 (
<VerticalPageLayout>
<IntersectionObserverProvider callback={callback}>
<Heading size="md" mt="2">
Special lists
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
<ListCard cord={`${kinds.Contacts}:${pubkey}`} hideCreator />
<ListCard cord={`${MUTE_LIST_KIND}:${pubkey}`} hideCreator />
<ListCard cord={`${PIN_LIST_KIND}:${pubkey}`} hideCreator />
<ListCard cord={`${BOOKMARK_LIST_KIND}:${pubkey}`} hideCreator />
</SimpleGrid>
<Heading size="md" mt="2">
Special lists
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
<ListCard cord={`${kinds.Contacts}:${pubkey}`} hideCreator />
<ListCard cord={`${kinds.Mutelist}:${pubkey}`} hideCreator />
<ListCard cord={`${kinds.Pinlist}:${pubkey}`} hideCreator />
<ListCard cord={`${kinds.BookmarkList}:${pubkey}`} hideCreator />
<ListCard cord={`${kinds.CommunitiesList}:${pubkey}`} hideCreator />
<ListCard cord={`${kinds.PublicChatsList}:${pubkey}`} hideCreator />
</SimpleGrid>
{peopleLists.length > 0 && (
<>
<Heading size="md" mt="2">
People lists
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
{peopleLists.map((event) => (
<ListCard key={getEventUID(event)} list={event} hideCreator />
))}
</SimpleGrid>
</>
)}
{followSets.length > 0 && (
<>
<Heading size="md" mt="2">
People lists
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
{followSets.map((set) => (
<ListCard key={getEventUID(set)} list={set} hideCreator />
))}
</SimpleGrid>
</>
)}
{noteLists.length > 0 && (
<>
<Heading size="md" mt="2">
Bookmark lists
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
{noteLists.map((event) => (
<ListCard key={getEventUID(event)} list={event} hideCreator />
))}
</SimpleGrid>
</>
)}
</IntersectionObserverProvider>
{genericSets.length > 0 && (
<>
<Heading size="md" mt="2">
Generic lists
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
{genericSets.map((set) => (
<ListCard key={getEventUID(set)} list={set} hideCreator />
))}
</SimpleGrid>
</>
)}
<IntersectionObserverProvider callback={callback}>
<Heading size="md" mt="2">
Lists <UserName pubkey={pubkey} /> is in
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
{otherLists.map((event) => (
<ListCard key={getEventUID(event)} list={event} />
))}
</SimpleGrid>
</IntersectionObserverProvider>
{bookmarkSets.length > 0 && (
<>
<Heading size="md" mt="2">
Bookmark sets
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
{bookmarkSets.map((set) => (
<ListCard key={getEventUID(set)} list={set} hideCreator />
))}
</SimpleGrid>
</>
)}
</VerticalPageLayout>
);
}

View File

@ -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(() => {