From 7fdd91d09533d9784527819f7a598ec9e4de9384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Tue, 31 Mar 2026 17:04:32 +0200 Subject: [PATCH] feat: bookmarks --- src/components/FavoriteSpellsDropdown.tsx | 15 +- src/components/layouts/AppShell.tsx | 4 + .../nostr/kinds/BaseEventRenderer.tsx | 60 ++++-- .../nostr/kinds/FavoriteSpellsRenderer.tsx | 10 +- src/components/nostr/kinds/SpellRenderer.tsx | 81 ++------ src/config/favorite-lists.ts | 56 ++++++ src/constants/kinds.ts | 1 - src/hooks/useEmojiSearchSync.ts | 3 + src/hooks/useFavoriteList.ts | 175 ++++++++++++++++++ src/hooks/useFavoriteListsSync.ts | 22 +++ src/hooks/useFavoriteSpells.ts | 123 ------------ src/types/spell.ts | 8 +- 12 files changed, 338 insertions(+), 220 deletions(-) create mode 100644 src/config/favorite-lists.ts create mode 100644 src/hooks/useFavoriteList.ts create mode 100644 src/hooks/useFavoriteListsSync.ts delete mode 100644 src/hooks/useFavoriteSpells.ts diff --git a/src/components/FavoriteSpellsDropdown.tsx b/src/components/FavoriteSpellsDropdown.tsx index 4cd1eb1..5ad86e7 100644 --- a/src/components/FavoriteSpellsDropdown.tsx +++ b/src/components/FavoriteSpellsDropdown.tsx @@ -1,6 +1,9 @@ +import { useMemo } from "react"; import { WandSparkles, Star } from "lucide-react"; import { useAccount } from "@/hooks/useAccount"; -import { useFavoriteSpells } from "@/hooks/useFavoriteSpells"; +import { useFavoriteList, getListPointers } from "@/hooks/useFavoriteList"; +import { FAVORITE_LISTS } from "@/config/favorite-lists"; +import { SPELL_KIND } from "@/constants/kinds"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { useAddWindow } from "@/core/state"; import { decodeSpell } from "@/lib/spell-conversion"; @@ -15,7 +18,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "./ui/dropdown-menu"; -import { FAVORITE_SPELLS_KIND } from "@/constants/kinds"; /** * A single spell item in the dropdown @@ -82,7 +84,12 @@ function FavoriteSpellItem({ pointer }: { pointer: EventPointer }) { */ export function FavoriteSpellsDropdown() { const { isLoggedIn, pubkey } = useAccount(); - const { favorites, event } = useFavoriteSpells(); + const spellConfig = FAVORITE_LISTS[SPELL_KIND]; + const { event } = useFavoriteList(spellConfig); + const favorites = useMemo( + () => (event ? getListPointers(event, "e") : []), + [event], + ); const addWindow = useAddWindow(); if (!isLoggedIn) return null; @@ -92,7 +99,7 @@ export function FavoriteSpellsDropdown() { // Open the user's kind 10777 event in detail view addWindow("open", { pointer: { - kind: FAVORITE_SPELLS_KIND, + kind: spellConfig.listKind, pubkey: pubkey!, identifier: "", }, diff --git a/src/components/layouts/AppShell.tsx b/src/components/layouts/AppShell.tsx index d0df932..cae5905 100644 --- a/src/components/layouts/AppShell.tsx +++ b/src/components/layouts/AppShell.tsx @@ -4,6 +4,7 @@ import { useAccountSync } from "@/hooks/useAccountSync"; import { useRelayListCacheSync } from "@/hooks/useRelayListCacheSync"; import { useBlossomServerCacheSync } from "@/hooks/useBlossomServerCacheSync"; import { useEmojiSearchSync } from "@/hooks/useEmojiSearchSync"; +import { useFavoriteListsSync } from "@/hooks/useFavoriteListsSync"; import { useRelayState } from "@/hooks/useRelayState"; import relayStateManager from "@/services/relay-state-manager"; import { TabBar } from "../TabBar"; @@ -34,6 +35,9 @@ export function AppShell({ children, hideBottomBar = false }: AppShellProps) { // Cache emoji lists (kind:10030) and emoji sets (kind:30030) for instant availability useEmojiSearchSync(); + // Pre-fetch all configured favorite lists (kind:10777, kind:10018, kind:10030) + useFavoriteListsSync(); + // Initialize global relay state manager useEffect(() => { relayStateManager.initialize(); diff --git a/src/components/nostr/kinds/BaseEventRenderer.tsx b/src/components/nostr/kinds/BaseEventRenderer.tsx index 42918ed..d822428 100644 --- a/src/components/nostr/kinds/BaseEventRenderer.tsx +++ b/src/components/nostr/kinds/BaseEventRenderer.tsx @@ -24,7 +24,8 @@ import { Zap, MessageSquare, SmilePlus, - Star, + Bookmark, + Loader2, } from "lucide-react"; import { useAddWindow, useGrimoire } from "@/core/state"; import { useCopy } from "@/hooks/useCopy"; @@ -46,8 +47,11 @@ import { ReactionBlueprint } from "@/lib/blueprints"; import { publishEventToRelays } from "@/services/hub"; import { selectRelaysForInteraction } from "@/services/relay-selection"; import type { EmojiTag } from "@/lib/emoji-helpers"; -import { useFavoriteSpells } from "@/hooks/useFavoriteSpells"; -import { SPELL_KIND } from "@/constants/kinds"; +import { useFavoriteList } from "@/hooks/useFavoriteList"; +import { + getFavoriteConfig, + FALLBACK_FAVORITE_CONFIG, +} from "@/config/favorite-lists"; /** * Universal event properties and utilities shared across all kind renderers @@ -139,9 +143,11 @@ export function EventMenu({ const addWindow = useAddWindow(); const { copy, copied } = useCopy(); const [jsonDialogOpen, setJsonDialogOpen] = useState(false); - const { isFavorite, toggleFavorite, isUpdating } = useFavoriteSpells(); - const isSpell = event.kind === SPELL_KIND; - const favorited = isSpell && isFavorite(event.id); + const favoriteConfig = getFavoriteConfig(event.kind); + const { isFavorite, toggleFavorite, isUpdating } = useFavoriteList( + favoriteConfig ?? FALLBACK_FAVORITE_CONFIG, + ); + const favorited = favoriteConfig ? isFavorite(event) : false; const openEventDetail = () => { let pointer; @@ -270,15 +276,22 @@ export function EventMenu({ React )} - {canSign && isSpell && ( + {canSign && favoriteConfig && ( toggleFavorite(event)} disabled={isUpdating} > - - {favorited ? "Remove from Favorites" : "Add to Favorites"} + {isUpdating ? ( + + ) : ( + + )} + {favorited ? "Unbookmark" : "Bookmark"} )} @@ -322,9 +335,11 @@ export function EventContextMenu({ const addWindow = useAddWindow(); const { copy, copied } = useCopy(); const [jsonDialogOpen, setJsonDialogOpen] = useState(false); - const { isFavorite, toggleFavorite, isUpdating } = useFavoriteSpells(); - const isSpell = event.kind === SPELL_KIND; - const favorited = isSpell && isFavorite(event.id); + const favoriteConfig = getFavoriteConfig(event.kind); + const { isFavorite, toggleFavorite, isUpdating } = useFavoriteList( + favoriteConfig ?? FALLBACK_FAVORITE_CONFIG, + ); + const favorited = favoriteConfig ? isFavorite(event) : false; const openEventDetail = () => { let pointer; @@ -449,15 +464,22 @@ export function EventContextMenu({ React )} - {canSign && isSpell && ( + {canSign && favoriteConfig && ( toggleFavorite(event)} disabled={isUpdating} > - - {favorited ? "Remove from Favorites" : "Add to Favorites"} + {isUpdating ? ( + + ) : ( + + )} + {favorited ? "Unbookmark" : "Bookmark"} )} diff --git a/src/components/nostr/kinds/FavoriteSpellsRenderer.tsx b/src/components/nostr/kinds/FavoriteSpellsRenderer.tsx index 30ee81b..b2a8322 100644 --- a/src/components/nostr/kinds/FavoriteSpellsRenderer.tsx +++ b/src/components/nostr/kinds/FavoriteSpellsRenderer.tsx @@ -5,7 +5,9 @@ import { ClickableEventTitle, } from "./BaseEventRenderer"; import { useNostrEvent } from "@/hooks/useNostrEvent"; -import { useFavoriteSpells, getSpellPointers } from "@/hooks/useFavoriteSpells"; +import { useFavoriteList, getListPointers } from "@/hooks/useFavoriteList"; +import { FAVORITE_LISTS } from "@/config/favorite-lists"; +import { SPELL_KIND } from "@/constants/kinds"; import { useAccount } from "@/hooks/useAccount"; import { useAddWindow } from "@/core/state"; import { decodeSpell } from "@/lib/spell-conversion"; @@ -19,7 +21,7 @@ import { Skeleton } from "@/components/ui/skeleton"; * Kind 10777 Renderer - Favorite Spells (Feed View) */ export function FavoriteSpellsRenderer({ event }: BaseEventProps) { - const pointers = getSpellPointers(event); + const pointers = getListPointers(event, "e"); return ( @@ -126,9 +128,9 @@ function SpellRefItem({ */ export function FavoriteSpellsDetailRenderer({ event }: { event: NostrEvent }) { const { canSign } = useAccount(); - const { toggleFavorite } = useFavoriteSpells(); + const { toggleFavorite } = useFavoriteList(FAVORITE_LISTS[SPELL_KIND]); - const pointers = getSpellPointers(event); + const pointers = getListPointers(event, "e"); return (
diff --git a/src/components/nostr/kinds/SpellRenderer.tsx b/src/components/nostr/kinds/SpellRenderer.tsx index 8e84427..63e7024 100644 --- a/src/components/nostr/kinds/SpellRenderer.tsx +++ b/src/components/nostr/kinds/SpellRenderer.tsx @@ -9,14 +9,12 @@ import { Badge } from "@/components/ui/badge"; import { KindBadge } from "@/components/KindBadge"; import { SpellEvent } from "@/types/spell"; import { CopyableJsonViewer } from "@/components/JsonViewer"; -import { User, Users, Star } from "lucide-react"; +import { User, Users } from "lucide-react"; import { cn } from "@/lib/utils"; import { UserName } from "../UserName"; import { useAddWindow, useGrimoire } from "@/core/state"; import { useProfile } from "@/hooks/useProfile"; import { useNostrEvent } from "@/hooks/useNostrEvent"; -import { useAccount } from "@/hooks/useAccount"; -import { useFavoriteSpells } from "@/hooks/useFavoriteSpells"; import { getDisplayName } from "@/lib/nostr-utils"; /** @@ -151,43 +149,21 @@ function IdentifierList({ * Displays spell name, description, and the reconstructed command */ export function SpellRenderer({ event }: BaseEventProps) { - const { canSign } = useAccount(); - const { isFavorite, toggleFavorite, isUpdating } = useFavoriteSpells(); - const favorited = isFavorite(event.id); - try { const spell = decodeSpell(event as SpellEvent); return (
- {/* Title + Favorite */} -
- {spell.name && ( - - {spell.name} - - )} - {canSign && ( - - )} -
+ {/* Title */} + {spell.name && ( + + {spell.name} + + )} {/* Description */} {spell.description && ( @@ -240,9 +216,6 @@ export function SpellRenderer({ event }: BaseEventProps) { export function SpellDetailRenderer({ event }: BaseEventProps) { const { state } = useGrimoire(); const activePubkey = state.activeAccount?.pubkey; - const { canSign } = useAccount(); - const { isFavorite, toggleFavorite, isUpdating } = useFavoriteSpells(); - const favorited = isFavorite(event.id); try { const spell = decodeSpell(event as SpellEvent); @@ -264,32 +237,14 @@ export function SpellDetailRenderer({ event }: BaseEventProps) { return (
-
- {spell.name && ( - - {spell.name} - - )} - {canSign && ( - - )} -
+ {spell.name && ( + + {spell.name} + + )} {spell.description && (

{spell.description}

)} diff --git a/src/config/favorite-lists.ts b/src/config/favorite-lists.ts new file mode 100644 index 0000000..9028907 --- /dev/null +++ b/src/config/favorite-lists.ts @@ -0,0 +1,56 @@ +import { SPELL_KIND } from "@/constants/kinds"; + +export interface FavoriteListConfig { + /** The replaceable list kind that stores favorites (e.g., 10777) */ + listKind: number; + /** The kind of events stored in the list (e.g., 777 for spells) */ + elementKind: number; + /** Human-readable label for UI */ + label: string; +} + +/** + * Maps event kind → favorite list configuration. + * + * Tag type ("e" vs "a") is derived at runtime from isAddressableKind(elementKind). + * To add a new favoritable kind, just add an entry here. + */ +export const FAVORITE_LISTS: Record = { + [SPELL_KIND]: { + listKind: 10777, + elementKind: SPELL_KIND, + label: "Favorite Spells", + }, + 30617: { + listKind: 10018, + elementKind: 30617, + label: "Favorite Repositories", + }, + 30030: { + listKind: 10030, + elementKind: 30030, + label: "Emoji Sets", + }, +}; + +/** + * Dummy config used as a stable fallback so hooks can be called unconditionally. + * Points at a kind combo that will never match real data. + */ +export const FALLBACK_FAVORITE_CONFIG: FavoriteListConfig = { + listKind: -1, + elementKind: -1, + label: "", +}; + +/** Look up config for a given event kind */ +export function getFavoriteConfig( + eventKind: number, +): FavoriteListConfig | undefined { + return FAVORITE_LISTS[eventKind]; +} + +/** All list kinds that need to be fetched at bootstrap */ +export const ALL_FAVORITE_LIST_KINDS = [ + ...new Set(Object.values(FAVORITE_LISTS).map((c) => c.listKind)), +]; diff --git a/src/constants/kinds.ts b/src/constants/kinds.ts index 93572e1..4740b10 100644 --- a/src/constants/kinds.ts +++ b/src/constants/kinds.ts @@ -94,7 +94,6 @@ export interface EventKind { } export const SPELL_KIND = 777; -export const FAVORITE_SPELLS_KIND = 10777; export const SPELLBOOK_KIND = 30777; export const EVENT_KINDS: Record = { diff --git a/src/hooks/useEmojiSearchSync.ts b/src/hooks/useEmojiSearchSync.ts index 2ae6af9..fdbe863 100644 --- a/src/hooks/useEmojiSearchSync.ts +++ b/src/hooks/useEmojiSearchSync.ts @@ -4,6 +4,9 @@ * Loads cached emojis from Dexie on startup (instant availability), * then subscribes to kind:10030 and kind:30030 events for live updates. * Should be used once at app root level (AppShell). + * + * NOTE: Network fetching of kind:10030 is handled by useFavoriteListsSync. + * This hook only subscribes to the EventStore observable for cache updates. */ import { useEffect } from "react"; diff --git a/src/hooks/useFavoriteList.ts b/src/hooks/useFavoriteList.ts new file mode 100644 index 0000000..0050f91 --- /dev/null +++ b/src/hooks/useFavoriteList.ts @@ -0,0 +1,175 @@ +import { useMemo, useState, useCallback, useRef } from "react"; +import { use$ } from "applesauce-react/hooks"; +import { + getEventPointerFromETag, + getAddressPointerFromATag, + getTagValue, +} from "applesauce-core/helpers"; +import { getSeenRelays } from "applesauce-core/helpers/relays"; +import { EventFactory } from "applesauce-core/event-factory"; +import eventStore from "@/services/event-store"; +import accountManager from "@/services/accounts"; +import { publishEvent } from "@/services/hub"; +import { useAccount } from "@/hooks/useAccount"; +import { isAddressableKind } from "@/lib/nostr-kinds"; +import type { FavoriteListConfig } from "@/config/favorite-lists"; +import type { NostrEvent } from "@/types/nostr"; +import type { EventPointer, AddressPointer } from "nostr-tools/nip19"; + +/** Compute the identity key for an event based on tag type */ +function getItemKey(event: NostrEvent, tagType: "e" | "a"): string { + if (tagType === "a") { + const dTag = getTagValue(event, "d") || ""; + return `${event.kind}:${event.pubkey}:${dTag}`; + } + return event.id; +} + +/** Extract pointers from tags of a given type */ +export function getListPointers( + event: NostrEvent, + tagType: "e", +): EventPointer[]; +export function getListPointers( + event: NostrEvent, + tagType: "a", +): AddressPointer[]; +export function getListPointers( + event: NostrEvent, + tagType: "e" | "a", +): EventPointer[] | AddressPointer[]; +export function getListPointers( + event: NostrEvent, + tagType: "e" | "a", +): (EventPointer | AddressPointer)[] { + const pointers: (EventPointer | AddressPointer)[] = []; + for (const tag of event.tags) { + if (tag[0] === tagType && tag[1]) { + if (tagType === "e") { + const pointer = getEventPointerFromETag(tag); + if (pointer) pointers.push(pointer); + } else { + const pointer = getAddressPointerFromATag(tag); + if (pointer) pointers.push(pointer); + } + } + } + return pointers; +} + +/** Build a tag for adding an item to a favorite list */ +function buildTag(event: NostrEvent, tagType: "e" | "a"): string[] { + const seenRelays = getSeenRelays(event); + const relayHint = seenRelays ? Array.from(seenRelays)[0] || "" : ""; + + if (tagType === "a") { + const dTag = getTagValue(event, "d") || ""; + const coordinate = `${event.kind}:${event.pubkey}:${dTag}`; + return relayHint ? ["a", coordinate, relayHint] : ["a", coordinate]; + } + + return relayHint ? ["e", event.id, relayHint] : ["e", event.id]; +} + +/** + * Generic hook to read and manage a NIP-51-style favorite list. + * + * Tag type ("e" vs "a") is derived from the element kind using isAddressableKind(). + */ +export function useFavoriteList(config: FavoriteListConfig) { + const { pubkey, canSign } = useAccount(); + const [isUpdating, setIsUpdating] = useState(false); + const isUpdatingRef = useRef(false); + + const tagType = isAddressableKind(config.elementKind) ? "a" : "e"; + + // Subscribe to the user's replaceable list event + const event = use$( + () => + pubkey ? eventStore.replaceable(config.listKind, pubkey, "") : undefined, + [pubkey, config.listKind], + ); + + // Extract pointers from matching tags + const items = useMemo( + () => (event ? getListPointers(event, tagType) : []), + [event, tagType], + ); + + // Quick lookup set of item identity keys + const itemIds = useMemo(() => { + if (!event) return new Set(); + const ids = new Set(); + for (const tag of event.tags) { + if (tag[0] === tagType && tag[1]) { + ids.add(tag[1]); + } + } + return ids; + }, [event, tagType]); + + const isFavorite = useCallback( + (targetEvent: NostrEvent) => { + const key = getItemKey(targetEvent, tagType); + return itemIds.has(key); + }, + [tagType, itemIds], + ); + + const toggleFavorite = useCallback( + async (targetEvent: NostrEvent) => { + if (!canSign || isUpdatingRef.current) return; + + const account = accountManager.active; + if (!account?.signer) return; + + isUpdatingRef.current = true; + setIsUpdating(true); + try { + const currentTags = event ? event.tags.map((t) => [...t]) : []; + const currentContent = event?.content ?? ""; + + const itemKey = getItemKey(targetEvent, tagType); + const alreadyFavorited = currentTags.some( + (t) => t[0] === tagType && t[1] === itemKey, + ); + + let newTags: string[][]; + if (alreadyFavorited) { + newTags = currentTags.filter( + (t) => !(t[0] === tagType && t[1] === itemKey), + ); + } else { + newTags = [...currentTags, buildTag(targetEvent, tagType)]; + } + + const factory = new EventFactory({ signer: account.signer }); + const built = await factory.build({ + kind: config.listKind, + content: currentContent, + tags: newTags, + }); + const signed = await factory.sign(built); + await publishEvent(signed); + } catch (err) { + console.error( + `[useFavoriteList] Failed to toggle favorite (list kind ${config.listKind}):`, + err, + ); + } finally { + isUpdatingRef.current = false; + setIsUpdating(false); + } + }, + [canSign, config, event, tagType], + ); + + return { + items, + itemIds, + isFavorite, + toggleFavorite, + isUpdating, + event, + }; +} diff --git a/src/hooks/useFavoriteListsSync.ts b/src/hooks/useFavoriteListsSync.ts new file mode 100644 index 0000000..2d13ce6 --- /dev/null +++ b/src/hooks/useFavoriteListsSync.ts @@ -0,0 +1,22 @@ +import { useEffect } from "react"; +import { useAccount } from "./useAccount"; +import { ALL_FAVORITE_LIST_KINDS } from "@/config/favorite-lists"; +import { addressLoader } from "@/services/loaders"; + +/** + * Fetch all configured favorite list kinds from relays at app boot, + * so they're available in EventStore before any UI needs them. + */ +export function useFavoriteListsSync() { + const { pubkey } = useAccount(); + + useEffect(() => { + if (!pubkey) return; + + const subs = ALL_FAVORITE_LIST_KINDS.map((listKind) => + addressLoader({ kind: listKind, pubkey, identifier: "" }).subscribe(), + ); + + return () => subs.forEach((s) => s.unsubscribe()); + }, [pubkey]); +} diff --git a/src/hooks/useFavoriteSpells.ts b/src/hooks/useFavoriteSpells.ts deleted file mode 100644 index 663e2c4..0000000 --- a/src/hooks/useFavoriteSpells.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { useMemo, useState, useCallback } from "react"; -import { use$ } from "applesauce-react/hooks"; -import { getEventPointerFromETag } from "applesauce-core/helpers"; -import { getSeenRelays } from "applesauce-core/helpers/relays"; -import { EventFactory } from "applesauce-core/event-factory"; -import eventStore from "@/services/event-store"; -import accountManager from "@/services/accounts"; -import { publishEvent } from "@/services/hub"; -import { useAccount } from "@/hooks/useAccount"; -import { FAVORITE_SPELLS_KIND } from "@/constants/kinds"; -import type { NostrEvent } from "@/types/nostr"; -import type { EventPointer } from "nostr-tools/nip19"; - -/** - * Extract EventPointers from "e" tags of a Nostr event. - * Shared by the hook and renderers. - */ -export function getSpellPointers(event: NostrEvent): EventPointer[] { - const pointers: EventPointer[] = []; - for (const tag of event.tags) { - if (tag[0] === "e" && tag[1]) { - const pointer = getEventPointerFromETag(tag); - if (pointer) pointers.push(pointer); - } - } - return pointers; -} - -/** - * Hook to read and manage the logged-in user's favorite spells (kind 10777). - * - * The kind 10777 event is a replaceable event containing "e" tags - * pointing to spell events (kind 777). - */ -export function useFavoriteSpells() { - const { pubkey, canSign } = useAccount(); - const [isUpdating, setIsUpdating] = useState(false); - - // Subscribe to the user's kind 10777 replaceable event - const event = use$( - () => - pubkey - ? eventStore.replaceable(FAVORITE_SPELLS_KIND, pubkey, "") - : undefined, - [pubkey], - ); - - // Extract event pointers from "e" tags - const favorites = useMemo( - () => (event ? getSpellPointers(event) : []), - [event], - ); - - // Quick lookup set of favorited event IDs - const favoriteIds = useMemo( - () => new Set(favorites.map((p) => p.id)), - [favorites], - ); - - const isFavorite = useCallback( - (eventId: string) => favoriteIds.has(eventId), - [favoriteIds], - ); - - const toggleFavorite = useCallback( - async (spellEvent: NostrEvent) => { - if (!canSign || isUpdating) return; - - const account = accountManager.active; - if (!account?.signer) return; - - setIsUpdating(true); - try { - // Start from the full existing event tags to preserve everything - const currentTags = event ? event.tags.map((t) => [...t]) : []; - const currentContent = event?.content ?? ""; - - const alreadyFavorited = currentTags.some( - (t) => t[0] === "e" && t[1] === spellEvent.id, - ); - - let newTags: string[][]; - if (alreadyFavorited) { - // Remove only the matching "e" tag - newTags = currentTags.filter( - (t) => !(t[0] === "e" && t[1] === spellEvent.id), - ); - } else { - // Add with relay hint - const seenRelays = getSeenRelays(spellEvent); - const relayHint = seenRelays ? Array.from(seenRelays)[0] || "" : ""; - const newTag = relayHint - ? ["e", spellEvent.id, relayHint] - : ["e", spellEvent.id]; - newTags = [...currentTags, newTag]; - } - - const factory = new EventFactory({ signer: account.signer }); - const built = await factory.build({ - kind: FAVORITE_SPELLS_KIND, - content: currentContent, - tags: newTags, - }); - const signed = await factory.sign(built); - await publishEvent(signed); - } catch (err) { - console.error("[useFavoriteSpells] Failed to toggle favorite:", err); - } finally { - setIsUpdating(false); - } - }, - [canSign, isUpdating, event], - ); - - return { - favorites, - favoriteIds, - isFavorite, - toggleFavorite, - isUpdating, - event, - }; -} diff --git a/src/types/spell.ts b/src/types/spell.ts index 7667b2e..9f85c7e 100644 --- a/src/types/spell.ts +++ b/src/types/spell.ts @@ -1,12 +1,8 @@ import type { NostrEvent, NostrFilter } from "./nostr"; import type { Workspace, WindowInstance } from "./app"; -import { - SPELL_KIND, - FAVORITE_SPELLS_KIND, - SPELLBOOK_KIND, -} from "@/constants/kinds"; +import { SPELL_KIND, SPELLBOOK_KIND } from "@/constants/kinds"; -export { SPELL_KIND, FAVORITE_SPELLS_KIND, SPELLBOOK_KIND }; +export { SPELL_KIND, SPELLBOOK_KIND }; /** * Spell event (kind 777 immutable event)