Add support for default bookmark list

This commit is contained in:
hzrd149 2023-12-01 12:53:57 -06:00
parent 9d939069c9
commit 2786f8487a
13 changed files with 92 additions and 23 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add support for default bookmark list

View File

@ -45,7 +45,7 @@ export default function PinNoteMenuItem({ event }: { event: NostrEvent }) {
if (event.pubkey !== account?.pubkey) return null;
return (
<MenuItem onClick={togglePin} icon={<PinIcon />} isDisabled={loading || !account?.readonly}>
<MenuItem onClick={togglePin} icon={<PinIcon />} isDisabled={loading || !!account?.readonly}>
{label}
</MenuItem>
);

View File

@ -10,7 +10,13 @@ import { NostrEvent } from "../../types/nostr-event";
import { STREAM_CHAT_MESSAGE_KIND, STREAM_KIND } from "../../helpers/nostr/stream";
import { GOAL_KIND } from "../../helpers/nostr/goal";
import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs";
import { NOTE_LIST_KIND, PEOPLE_LIST_KIND } from "../../helpers/nostr/lists";
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 { STEMSTR_TRACK_KIND } from "../../helpers/nostr/stemstr";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
@ -58,6 +64,9 @@ export function EmbedEvent({
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 Kind.Article:
return <EmbeddedArticle article={event} {...cardProps} />;

View File

@ -12,6 +12,7 @@ import {
useDisclosure,
useToast,
} from "@chakra-ui/react";
import dayjs from "dayjs";
import useCurrentAccount from "../../../hooks/use-current-account";
import { useSigningContext } from "../../../providers/signing-provider";
@ -22,14 +23,16 @@ import {
listRemoveEvent,
getEventsFromList,
getListName,
BOOKMARK_LIST_KIND,
} from "../../../helpers/nostr/lists";
import { NostrEvent } from "../../../types/nostr-event";
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
import { getEventCoordinate } from "../../../helpers/nostr/events";
import clientRelaysService from "../../../services/client-relays";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import { BookmarkIcon, BookmarkedIcon, PlusCircleIcon } from "../../icons";
import NewListModal from "../../../views/lists/components/new-list-modal";
import replaceableEventLoaderService from "../../../services/replaceable-event-requester";
import userUserBookmarksList from "../../../hooks/use-user-bookmarks-list";
export default function BookmarkButton({ event, ...props }: { event: NostrEvent } & Omit<IconButtonProps, "icon">) {
const toast = useToast();
@ -38,8 +41,37 @@ export default function BookmarkButton({ event, ...props }: { event: NostrEvent
const { requestSignature } = useSigningContext();
const [isLoading, setLoading] = useState(false);
const { list: bookmarkList, pointers: bookmarkPointers } = userUserBookmarksList();
const lists = useUserLists(account?.pubkey).filter((list) => list.kind === NOTE_LIST_KIND);
const isBookmarked = bookmarkPointers.some((p) => p.id === event.id);
const handleBookmarkClick = useCallback(async () => {
const writeRelays = clientRelaysService.getWriteUrls();
setLoading(true);
try {
let draft: DraftNostrEvent = {
kind: BOOKMARK_LIST_KIND,
content: bookmarkList?.content ?? "",
tags: bookmarkList?.tags ?? [],
created_at: dayjs().unix(),
};
if (isBookmarked) {
draft = listRemoveEvent(draft, event.id);
const signed = await requestSignature(draft);
new NostrPublishAction("Remove Bookmark", writeRelays, signed);
} else {
draft = listAddEvent(draft, event.id);
const signed = await requestSignature(draft);
new NostrPublishAction("Bookmark Note", writeRelays, signed);
}
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
setLoading(false);
}, [event.id, requestSignature, bookmarkList, isBookmarked]);
const inLists = lists.filter((list) => getEventsFromList(list).some((p) => p.id === event.id));
const handleChange = useCallback(
@ -79,11 +111,19 @@ export default function BookmarkButton({ event, ...props }: { event: NostrEvent
<Menu isLazy closeOnSelect={false}>
<MenuButton
as={IconButton}
icon={inLists.length > 0 ? <BookmarkedIcon /> : <BookmarkIcon />}
icon={inLists.length > 0 || isBookmarked ? <BookmarkedIcon /> : <BookmarkIcon />}
isDisabled={account?.readonly ?? true}
{...props}
/>
<MenuList minWidth="240px">
<MenuItem
icon={isBookmarked ? <BookmarkedIcon /> : <BookmarkIcon />}
isDisabled={account?.readonly || isLoading}
onClick={handleBookmarkClick}
>
Bookmark
</MenuItem>
<MenuDivider />
{lists.length > 0 && (
<MenuOptionGroup
type="checkbox"
@ -94,7 +134,7 @@ export default function BookmarkButton({ event, ...props }: { event: NostrEvent
<MenuItemOption
key={getEventCoordinate(list)}
value={getEventCoordinate(list)}
isDisabled={account?.readonly && isLoading}
isDisabled={account?.readonly || isLoading}
isTruncated
maxW="90vw"
>

View File

@ -78,8 +78,8 @@ export function getSharableEventAddress(event: NostrEvent) {
return nip19.naddrEncode({ kind: event.kind, identifier: d, pubkey: event.pubkey, relays: maxTwo });
} else {
if (maxTwo.length == 2) {
return nip19.neventEncode({ id: event.id, relays: maxTwo });
} else return nip19.neventEncode({ id: event.id, relays: maxTwo, author: event.pubkey });
return nip19.neventEncode({ id: event.id, kind: event.kind, relays: maxTwo });
} else return nip19.neventEncode({ id: event.id, kind: event.kind, relays: maxTwo, author: event.pubkey });
}
}

View File

@ -1,8 +1,6 @@
import { nip19 } from "nostr-tools";
import { NostrEvent, isETag } from "../../types/nostr-event";
export const USER_CHANNELS_LIST_KIND = 10005;
export type ChannelMetadata = {
name: string;
about: string;

View File

@ -9,7 +9,7 @@ export const MUTE_LIST_KIND = 10000;
export const PIN_LIST_KIND = 10001;
export const BOOKMARK_LIST_KIND = 10003;
export const COMMUNITIES_LIST_KIND = 10004;
export const CHATS_LIST_KIND = 10005;
export const CHANNELS_LIST_KIND = 10005;
export const PEOPLE_LIST_KIND = 30000;
export const NOTE_LIST_KIND = 30001;
@ -44,7 +44,7 @@ export function isSpecialListKind(kind: number) {
kind === PIN_LIST_KIND ||
kind === BOOKMARK_LIST_KIND ||
kind === COMMUNITIES_LIST_KIND ||
kind === CHATS_LIST_KIND
kind === CHANNELS_LIST_KIND
);
}

View File

@ -0,0 +1,15 @@
import { BOOKMARK_LIST_KIND, getEventsFromList } from "../helpers/nostr/lists";
import { RequestOptions } from "../services/replaceable-event-requester";
import useCurrentAccount from "./use-current-account";
import useReplaceableEvent from "./use-replaceable-event";
export default function userUserBookmarksList(pubkey?: string, relays: string[] = [], opts?: RequestOptions) {
const account = useCurrentAccount();
const key = pubkey ?? account?.pubkey;
const list = useReplaceableEvent(key ? { kind: BOOKMARK_LIST_KIND, pubkey: key } : undefined, relays, opts);
const pointers = list ? getEventsFromList(list) : [];
return { list, pointers };
}

View File

@ -1,5 +1,4 @@
import { USER_CHANNELS_LIST_KIND } from "../helpers/nostr/channel";
import { getEventsFromList } from "../helpers/nostr/lists";
import { CHANNELS_LIST_KIND, getEventsFromList } from "../helpers/nostr/lists";
import { RequestOptions } from "../services/replaceable-event-requester";
import useCurrentAccount from "./use-current-account";
import useReplaceableEvent from "./use-replaceable-event";
@ -8,7 +7,7 @@ export default function useUserChannelsList(pubkey?: string, relays: string[] =
const account = useCurrentAccount();
const key = pubkey ?? account?.pubkey;
const list = useReplaceableEvent(key ? { kind: USER_CHANNELS_LIST_KIND, pubkey: key } : undefined, relays, opts);
const list = useReplaceableEvent(key ? { kind: CHANNELS_LIST_KIND, pubkey: key } : undefined, relays, opts);
const pointers = list ? getEventsFromList(list) : [];

View File

@ -2,14 +2,13 @@ import { useCallback } from "react";
import dayjs from "dayjs";
import { Button, ButtonProps, useToast } from "@chakra-ui/react";
import { DraftNostrEvent, NostrEvent, isDTag, isETag } from "../../../types/nostr-event";
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
import useCurrentAccount from "../../../hooks/use-current-account";
import { listAddEvent, listRemoveEvent } from "../../../helpers/nostr/lists";
import { CHANNELS_LIST_KIND, listAddEvent, listRemoveEvent } from "../../../helpers/nostr/lists";
import { useSigningContext } from "../../../providers/signing-provider";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import clientRelaysService from "../../../services/client-relays";
import useUserChannelsList from "../../../hooks/use-user-channels-list";
import { USER_CHANNELS_LIST_KIND } from "../../../helpers/nostr/channel";
export default function ChannelJoinButton({
channel,
@ -25,7 +24,7 @@ export default function ChannelJoinButton({
const handleClick = useCallback(async () => {
try {
const favList = {
kind: USER_CHANNELS_LIST_KIND,
kind: CHANNELS_LIST_KIND,
content: list?.content ?? "",
created_at: dayjs().unix(),
tags: list?.tags ?? [],

View File

@ -19,7 +19,6 @@ import {
import { NostrEvent } from "../../../types/nostr-event";
import useChannelMetadata from "../../../hooks/use-channel-metadata";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import { USER_CHANNELS_LIST_KIND } from "../../../helpers/nostr/channel";
import useSubject from "../../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../../providers/intersection-observer";
@ -30,6 +29,7 @@ import { useRelaySelectionContext } from "../../../providers/relay-selection-pro
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
import ChannelJoinButton from "./channel-join-button";
import { ExternalLinkIcon } from "../../../components/icons";
import { CHANNELS_LIST_KIND } from "../../../helpers/nostr/lists";
function UserCard({ pubkey }: { pubkey: string }) {
return (
@ -42,7 +42,7 @@ function UserCard({ pubkey }: { pubkey: string }) {
}
function ChannelMembers({ channel, relays }: { channel: NostrEvent; relays: string[] }) {
const timeline = useTimelineLoader(`${channel.id}-members`, relays, {
kinds: [USER_CHANNELS_LIST_KIND],
kinds: [CHANNELS_LIST_KIND],
"#e": [channel.id],
});
const userLists = useSubject(timeline.timeline);

View File

@ -31,7 +31,6 @@ function NostrLinkPage() {
return <Navigate to={`/n/${cleanLink}`} replace />;
case "nevent":
case "naddr":
if (decoded.data.kind === Kind.Text) return <Navigate to={`/n/${cleanLink}`} replace />;
if (decoded.data.kind === TORRENT_KIND) return <Navigate to={`/torrents/${cleanLink}`} replace />;
if (decoded.data.kind === STREAM_KIND) return <Navigate to={`/streams/${cleanLink}`} replace />;
if (decoded.data.kind === EMOJI_PACK_KIND) return <Navigate to={`/emojis/${cleanLink}`} replace />;
@ -40,12 +39,15 @@ function NostrLinkPage() {
if (decoded.data.kind === Kind.BadgeDefinition) return <Navigate to={`/badges/${cleanLink}`} replace />;
if (decoded.data.kind === COMMUNITY_DEFINITION_KIND) return <Navigate to={`/c/${cleanLink}`} replace />;
if (decoded.data.kind === Kind.ChannelCreation) return <Navigate to={`/channels/${cleanLink}`} replace />;
if (decoded.data.kind === Kind.Text) return <Navigate to={`/n/${cleanLink}`} replace />;
// if there is no kind redirect to the thread view
return <Navigate to={`/n/${cleanLink}`} replace />;
}
return (
<Alert status="warning">
<AlertIcon />
<AlertTitle>Unknown type</AlertTitle>
<AlertTitle>Unknown type {JSON.stringify(decoded.data)}</AlertTitle>
</Alert>
);
}

View File

@ -8,11 +8,13 @@ import { getSharableEventAddress } from "../../../helpers/nip19";
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";
export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
const infoModal = useDisclosure();
const naddr = getSharableEventAddress(list);
const isSpecial = isSpecialListKind(list.kind);
const hasPeople = list.tags.some(isPTag);
@ -21,7 +23,7 @@ export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit
<CustomMenuIconButton {...props}>
<OpenInAppMenuItem event={list} />
<CopyEmbedCodeMenuItem event={list} />
<DeleteEventMenuItem event={list} label="Delete List" />
{!isSpecial && <DeleteEventMenuItem event={list} label="Delete List" />}
{hasPeople && (
<MenuItem
icon={<Image w="4" h="4" src="https://www.makeprisms.com/favicon.ico" />}