mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
Add support for default bookmark list
This commit is contained in:
parent
9d939069c9
commit
2786f8487a
5
.changeset/dull-rivers-wave.md
Normal file
5
.changeset/dull-rivers-wave.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add support for default bookmark list
|
@ -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>
|
||||
);
|
||||
|
@ -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} />;
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
15
src/hooks/use-user-bookmarks-list.ts
Normal file
15
src/hooks/use-user-bookmarks-list.ts
Normal 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 };
|
||||
}
|
@ -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) : [];
|
||||
|
||||
|
@ -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 ?? [],
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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" />}
|
||||
|
Loading…
x
Reference in New Issue
Block a user