diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx index 8857dcd..3a1eecc 100644 --- a/src/components/ChatViewer.tsx +++ b/src/components/ChatViewer.tsx @@ -47,7 +47,7 @@ import { RelaysDropdown } from "./chat/RelaysDropdown"; import { MessageReactions } from "./chat/MessageReactions"; import { StatusBadge } from "./live/StatusBadge"; import { ChatMessageContextMenu } from "./chat/ChatMessageContextMenu"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { Button } from "./ui/button"; import LoginDialog from "./nostr/LoginDialog"; import { @@ -512,7 +512,7 @@ export function ChatViewer({ customTitle, headerPrefix, }: ChatViewerProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); // Get active account with signing capability const { pubkey, canSign, signer } = useAccount(); diff --git a/src/components/Command.tsx b/src/components/Command.tsx index 480f383..ed842b0 100644 --- a/src/components/Command.tsx +++ b/src/components/Command.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { manPages } from "@/types/man"; import { AppId } from "@/types/app"; @@ -21,7 +21,7 @@ export default function Command({ commandLine, }: CommandProps) { const [showTooltip, setShowTooltip] = useState(false); - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const handleClick = async () => { if (commandLine) { diff --git a/src/components/DecodeViewer.tsx b/src/components/DecodeViewer.tsx index 9a15a3e..821f013 100644 --- a/src/components/DecodeViewer.tsx +++ b/src/components/DecodeViewer.tsx @@ -6,7 +6,7 @@ import { reencodeWithRelays, type DecodedData, } from "@/lib/decode-parser"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { useCopy } from "../hooks/useCopy"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; @@ -18,7 +18,7 @@ interface DecodeViewerProps { } export default function DecodeViewer({ args }: DecodeViewerProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const { copy, copied } = useCopy(); const [relays, setRelays] = useState([]); const [newRelay, setNewRelay] = useState(""); diff --git a/src/components/EventFooter.tsx b/src/components/EventFooter.tsx index 45b7098..1e76f7c 100644 --- a/src/components/EventFooter.tsx +++ b/src/components/EventFooter.tsx @@ -2,7 +2,7 @@ import { NostrEvent } from "@/types/nostr"; import { KindBadge } from "./KindBadge"; import { Wifi } from "lucide-react"; import { getSeenRelays } from "applesauce-core/helpers/relays"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { getKindName } from "@/constants/kinds"; import { DropdownMenu, @@ -22,7 +22,7 @@ interface EventFooterProps { * Right: Relay count dropdown */ export function EventFooter({ event }: EventFooterProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); // Get relays this event was seen on const seenRelaysSet = getSeenRelays(event); diff --git a/src/components/KindBadge.tsx b/src/components/KindBadge.tsx index e2069b7..68e20ac 100644 --- a/src/components/KindBadge.tsx +++ b/src/components/KindBadge.tsx @@ -1,6 +1,6 @@ import { getKindInfo } from "@/constants/kinds"; import { cn } from "@/lib/utils"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; interface KindBadgeProps { kind: number; @@ -23,7 +23,7 @@ export function KindBadge({ iconClassname = "text-muted-foreground", clickable = false, }: KindBadgeProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const kindInfo = getKindInfo(kind); const Icon = kindInfo?.icon; diff --git a/src/components/ManPage.tsx b/src/components/ManPage.tsx index ecec096..211f1e6 100644 --- a/src/components/ManPage.tsx +++ b/src/components/ManPage.tsx @@ -1,5 +1,5 @@ import { manPages } from "@/types/man"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { CenteredContent } from "./ui/CenteredContent"; import { cn } from "@/lib/utils"; import { Button } from "./ui/button"; @@ -22,7 +22,7 @@ export function ExecutableCommand({ children: React.ReactNode; spellId?: string; }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const handleClick = async () => { const parts = commandLine.trim().split(/\s+/); diff --git a/src/components/NIPBadge.tsx b/src/components/NIPBadge.tsx index c642787..b4a253b 100644 --- a/src/components/NIPBadge.tsx +++ b/src/components/NIPBadge.tsx @@ -1,5 +1,5 @@ import { getNIPInfo } from "../lib/nip-icons"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { isNipDeprecated } from "@/constants/nips"; export interface NIPBadgeProps { @@ -19,7 +19,7 @@ export function NIPBadge({ showName = true, showNIPPrefix = true, }: NIPBadgeProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const nipInfo = getNIPInfo(nipNumber); const name = nipInfo?.name || `NIP-${nipNumber}`; const description = diff --git a/src/components/NipsViewer.tsx b/src/components/NipsViewer.tsx index de49749..e01ee2a 100644 --- a/src/components/NipsViewer.tsx +++ b/src/components/NipsViewer.tsx @@ -4,7 +4,7 @@ import { VALID_NIPS, NIP_TITLES } from "@/constants/nips"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { NIPBadge } from "./NIPBadge"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { CenteredContent } from "./ui/CenteredContent"; /** @@ -14,7 +14,7 @@ import { CenteredContent } from "./ui/CenteredContent"; export default function NipsViewer() { const [search, setSearch] = useState(""); const searchInputRef = useRef(null); - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); // Autofocus on mount useEffect(() => { diff --git a/src/components/ReqViewer.tsx b/src/components/ReqViewer.tsx index 2f9cb76..ed687b6 100644 --- a/src/components/ReqViewer.tsx +++ b/src/components/ReqViewer.tsx @@ -25,7 +25,7 @@ import { import { Virtuoso } from "react-virtuoso"; import { WindowInstance } from "@/types/app"; import { useReqTimelineEnhanced } from "@/hooks/useReqTimelineEnhanced"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow, useGrimoire } from "@/core/state"; import { useRelayState } from "@/hooks/useRelayState"; import { useOutboxRelays } from "@/hooks/useOutboxRelays"; import { AGGREGATOR_RELAYS } from "@/services/loaders"; @@ -99,7 +99,7 @@ const MemoizedFeedEvent = memo( * Shows truncated ID with click to open */ function EventIdPreview({ eventId }: { eventId: string }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const handleClick = useCallback(() => { addWindow("open", { pointer: { id: eventId } }); @@ -727,7 +727,8 @@ export default function ReqViewer({ title = "nostr-events", windowId = "pop-up", }: ReqViewerProps) { - const { state, addWindow, updateWindow } = useGrimoire(); + const { state, updateWindow } = useGrimoire(); + const addWindow = useAddWindow(); const { relays: relayStates } = useRelayState(); // Get active account for alias resolution diff --git a/src/components/SpellsViewer.tsx b/src/components/SpellsViewer.tsx index 0c32096..7d8177a 100644 --- a/src/components/SpellsViewer.tsx +++ b/src/components/SpellsViewer.tsx @@ -31,7 +31,7 @@ import type { LocalSpell } from "@/services/db"; import { ExecutableCommand } from "./ManPage"; import { PublishSpellAction } from "@/actions/publish-spell"; import { DeleteEventAction } from "@/actions/delete-event"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow, useGrimoire } from "@/core/state"; import { cn } from "@/lib/utils"; import { KindBadge } from "@/components/KindBadge"; import { parseReqCommand } from "@/lib/req-parser"; @@ -47,7 +47,7 @@ interface SpellCardProps { } function SpellCard({ spell, onDelete, onPublish }: SpellCardProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const [isPublishing, setIsPublishing] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const displayName = spell.name || spell.alias || "Untitled Spell"; diff --git a/src/components/chat/ChatMessageContextMenu.tsx b/src/components/chat/ChatMessageContextMenu.tsx index b4e27b4..fb59aa5 100644 --- a/src/components/chat/ChatMessageContextMenu.tsx +++ b/src/components/chat/ChatMessageContextMenu.tsx @@ -20,7 +20,7 @@ import { Smile, Zap, } from "lucide-react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { useCopy } from "@/hooks/useCopy"; import { JsonViewer } from "@/components/JsonViewer"; import { KindBadge } from "@/components/KindBadge"; @@ -59,7 +59,7 @@ export function ChatMessageContextMenu({ adapter, message, }: ChatMessageContextMenuProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const { copy, copied } = useCopy(); const [jsonDialogOpen, setJsonDialogOpen] = useState(false); const [emojiPickerOpen, setEmojiPickerOpen] = useState(false); diff --git a/src/components/nostr/CompactEventRow.tsx b/src/components/nostr/CompactEventRow.tsx index 34dba48..3365c1c 100644 --- a/src/components/nostr/CompactEventRow.tsx +++ b/src/components/nostr/CompactEventRow.tsx @@ -1,7 +1,7 @@ import { memo, useCallback } from "react"; import type { NostrEvent } from "@/types/nostr"; import { kinds } from "nostr-tools"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow, useGrimoire } from "@/core/state"; import { formatTimestamp } from "@/hooks/useLocale"; import { getTagValue } from "applesauce-core/helpers"; import { getZapSender } from "applesauce-common/helpers/zap"; @@ -24,7 +24,7 @@ interface CompactEventRowProps { * Layout: [KindBadge] [Author] [Preview] [Time] */ export function CompactEventRow({ event }: CompactEventRowProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const { locale } = useGrimoire(); // Get the compact preview renderer for this kind, or use default diff --git a/src/components/nostr/CompactMediaRenderer.tsx b/src/components/nostr/CompactMediaRenderer.tsx index 8fc04ae..e64f3d6 100644 --- a/src/components/nostr/CompactMediaRenderer.tsx +++ b/src/components/nostr/CompactMediaRenderer.tsx @@ -11,7 +11,7 @@ import { useState } from "react"; import { Image, Video, Music, File, HardDrive } from "lucide-react"; import { getHashFromURL } from "blossom-client-sdk/helpers/url"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { formatFileSize } from "@/lib/imeta"; import { Tooltip, @@ -112,7 +112,7 @@ function MediaIcon({ type }: { type: "image" | "video" | "audio" }) { } export function CompactMediaRenderer({ url, type, imeta }: MediaRendererProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const [expanded, setExpanded] = useState(false); const truncatedHash = getTruncatedHash(url); diff --git a/src/components/nostr/GroupLink.tsx b/src/components/nostr/GroupLink.tsx index 35036af..71d55bf 100644 --- a/src/components/nostr/GroupLink.tsx +++ b/src/components/nostr/GroupLink.tsx @@ -1,5 +1,5 @@ import { MessageSquare } from "lucide-react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { cn } from "@/lib/utils"; import { getTagValue } from "applesauce-core/helpers"; import type { NostrEvent } from "@/types/nostr"; @@ -34,7 +34,7 @@ export function GroupLink({ className, iconClassname, }: GroupLinkProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); // Handle special case: "_" is the unmanaged relay top-level group const isUnmanagedGroup = groupId === "_"; diff --git a/src/components/nostr/MarkdownContent.tsx b/src/components/nostr/MarkdownContent.tsx index e0f087e..670cbbe 100644 --- a/src/components/nostr/MarkdownContent.tsx +++ b/src/components/nostr/MarkdownContent.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useMemo, useCallback } from "react"; import ReactMarkdown, { defaultUrlTransform } from "react-markdown"; import remarkGfm from "remark-gfm"; import { remarkNostrMentions } from "applesauce-content/markdown"; @@ -9,13 +9,13 @@ import { MediaEmbed } from "./MediaEmbed"; import { SyntaxHighlight } from "@/components/SyntaxHighlight"; import { CodeCopyButton } from "@/components/CodeCopyButton"; import { useCopy } from "@/hooks/useCopy"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; /** * Component to render nostr: mentions inline */ function NostrMention({ href }: { href: string }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); try { // Remove nostr: prefix and any trailing characters @@ -155,6 +155,14 @@ export interface MarkdownContentProps { canonicalUrl?: string | null; } +// Stable module-level constants — never recreated between renders +const REMARK_PLUGINS = [remarkGfm, remarkNostrMentions]; + +function urlTransform(url: string) { + if (url.startsWith("nostr:")) return url; + return defaultUrlTransform(url); +} + /** * Shared markdown renderer for Nostr content (articles, NIPs, etc.) * Handles nostr: mentions, syntax highlighting, media embeds, and relative URLs @@ -163,178 +171,183 @@ export function MarkdownContent({ content, canonicalUrl = null, }: MarkdownContentProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); // Helper to resolve relative URLs using canonical URL as base - const resolveUrl = useMemo( - () => - (url: string): string | null => { - // If it's already absolute, return as-is - if (url.match(/^https?:\/\//)) { - return url; + const resolveUrl = useCallback( + (url: string): string | null => { + // If it's already absolute, return as-is + if (url.match(/^https?:\/\//)) { + return url; + } + + // If we have a canonical URL, try to resolve relative URLs + if (canonicalUrl) { + try { + return new URL(url, canonicalUrl).toString(); + } catch { + console.warn("Failed to resolve relative URL:", url); + return null; + } + } + + // No canonical URL and it's relative - can't resolve + return null; + }, + [canonicalUrl], + ); + + // Memoize the processed content string + const processedContent = useMemo( + () => content.replace(/\\n/g, "\n"), + [content], + ); + + // Memoize components object to prevent ReactMarkdown from remounting children + const components = useMemo( + () => ({ + // Enable images with zoom + img: ({ src, alt }: { src?: string; alt?: string }) => { + if (!src) return null; + + const resolvedUrl = resolveUrl(src); + if (!resolvedUrl) { + // Can't resolve URL - show fallback + return ( +
+

Media unavailable (relative URL without base)

+

{src}

+
+ ); } - // If we have a canonical URL, try to resolve relative URLs - if (canonicalUrl) { - try { - return new URL(url, canonicalUrl).toString(); - } catch { - console.warn("Failed to resolve relative URL:", url); - return null; + return ( + + ); + }, + // Handle links: nostr mentions, NIP links, and regular URLs + a: ({ + href, + children, + ...props + }: React.AnchorHTMLAttributes) => { + if (!href) return null; + + // Render nostr: mentions inline + if (href.startsWith("nostr:")) { + return ; + } + + // Check if it's a relative NIP link (e.g., "./01.md" or "01.md") + const isRelativeLink = + !href.startsWith("http://") && !href.startsWith("https://"); + if (isRelativeLink && (href.endsWith(".md") || href.includes(".md#"))) { + // Extract NIP number from various formats (numeric 1-3 digits or hex A0-FF) + const nipMatch = href.match(/([0-9A-F]{1,3})\.md/i); + if (nipMatch) { + const nipNumber = nipMatch[1].toUpperCase(); + return ( + { + e.preventDefault(); + e.stopPropagation(); + addWindow("nip", { number: nipNumber }, `NIP ${nipNumber}`); + }} + className="text-accent underline decoration-dotted cursor-crosshair hover:text-accent/80" + > + {children} + + ); } } - // No canonical URL and it's relative - can't resolve - return null; + // Regular links with break-all for long URLs + return ( + + {children} + + ); }, - [canonicalUrl], + // Don't render pre wrapper when we have a CodeBlock (it has its own container) + pre: ({ children }: { children?: React.ReactNode }) => <>{children}, + // Style adjustments for dark theme + h1: (props: React.HTMLAttributes) => ( +

+ ), + h2: (props: React.HTMLAttributes) => ( +

+ ), + h3: (props: React.HTMLAttributes) => ( +

+ ), + p: (props: React.HTMLAttributes) => ( +

+ ), + code: ({ className, children, ...props }: any) => { + const match = /language-(\w+)/.exec(className || ""); + const language = match ? match[1] : null; + const code = String(children).replace(/\n$/, ""); + + // Inline code (no language) + if (!language) { + return ( + + {children} + + ); + } + + // Block code with syntax highlighting and copy button + return ; + }, + blockquote: (props: React.HTMLAttributes) => ( +

+ ), + ul: (props: React.HTMLAttributes) => ( +
    + ), + ol: (props: React.HTMLAttributes) => ( +
      + ), + hr: () =>
      , + }), + [resolveUrl, addWindow], ); return (
      { - if (url.startsWith("nostr:")) return url; - return defaultUrlTransform(url); - }} - components={{ - // Enable images with zoom - img: ({ src, alt }) => { - if (!src) return null; - - const resolvedUrl = resolveUrl(src); - if (!resolvedUrl) { - // Can't resolve URL - show fallback - return ( -
      -

      Media unavailable (relative URL without base)

      -

      {src}

      -
      - ); - } - - return ( - - ); - }, - // Handle links: nostr mentions, NIP links, and regular URLs - a: ({ href, children, ...props }) => { - if (!href) return null; - - // Render nostr: mentions inline - if (href.startsWith("nostr:")) { - return ; - } - - // Check if it's a relative NIP link (e.g., "./01.md" or "01.md") - const isRelativeLink = - !href.startsWith("http://") && !href.startsWith("https://"); - if ( - isRelativeLink && - (href.endsWith(".md") || href.includes(".md#")) - ) { - // Extract NIP number from various formats (numeric 1-3 digits or hex A0-FF) - const nipMatch = href.match(/([0-9A-F]{1,3})\.md/i); - if (nipMatch) { - const nipNumber = nipMatch[1].toUpperCase(); - return ( - { - e.preventDefault(); - e.stopPropagation(); - addWindow( - "nip", - { number: nipNumber }, - `NIP ${nipNumber}`, - ); - }} - className="text-accent underline decoration-dotted cursor-crosshair hover:text-accent/80" - > - {children} - - ); - } - } - - // Regular links with break-all for long URLs - return ( - - {children} - - ); - }, - // Don't render pre wrapper when we have a CodeBlock (it has its own container) - pre: ({ children }) => <>{children}, - // Style adjustments for dark theme - h1: ({ ...props }) => ( -

      - ), - h2: ({ ...props }) => ( -

      - ), - h3: ({ ...props }) => ( -

      - ), - p: ({ ...props }) => ( -

      - ), - code: ({ className, children, ...props }: any) => { - const match = /language-(\w+)/.exec(className || ""); - const language = match ? match[1] : null; - const code = String(children).replace(/\n$/, ""); - - // Inline code (no language) - if (!language) { - return ( - - {children} - - ); - } - - // Block code with syntax highlighting and copy button - return ; - }, - blockquote: ({ ...props }) => ( -

      - ), - ul: ({ ...props }) => ( -
        - ), - ol: ({ ...props }) => ( -
          - ), - hr: () =>
          , - }} + urlTransform={urlTransform} + components={components} > - {content.replace(/\\n/g, "\n")} + {processedContent}

      ); diff --git a/src/components/nostr/RelayLink.tsx b/src/components/nostr/RelayLink.tsx index e3df092..5a74bf7 100644 --- a/src/components/nostr/RelayLink.tsx +++ b/src/components/nostr/RelayLink.tsx @@ -1,5 +1,5 @@ import { Inbox, Send, ShieldAlert } from "lucide-react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { useRelayInfo } from "@/hooks/useRelayInfo"; import { HoverCard, @@ -50,7 +50,7 @@ export function RelayLink({ className, variant = "default", }: RelayLinkProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const relayInfo = useRelayInfo(url); const handleClick = () => { diff --git a/src/components/nostr/RepositoryLink.tsx b/src/components/nostr/RepositoryLink.tsx index 85a8a5e..f730c94 100644 --- a/src/components/nostr/RepositoryLink.tsx +++ b/src/components/nostr/RepositoryLink.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { FolderGit2 } from "lucide-react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { getRepositoryName, @@ -41,7 +41,7 @@ export function RepositoryLink({ inline = false, showIcon = true, }: RepositoryLinkProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); // Parse repository address to get the pointer (if not provided directly) const repoPointer = useMemo(() => { diff --git a/src/components/nostr/RichText/Nip.tsx b/src/components/nostr/RichText/Nip.tsx index 9204f22..a80ab96 100644 --- a/src/components/nostr/RichText/Nip.tsx +++ b/src/components/nostr/RichText/Nip.tsx @@ -1,5 +1,5 @@ import type { NipNode } from "@/lib/nip-transformer"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { getNIPInfo } from "@/lib/nip-icons"; interface NipNodeProps { @@ -10,7 +10,7 @@ interface NipNodeProps { * Renders a NIP reference as a clickable link that opens the NIP viewer */ export function Nip({ node }: NipNodeProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const { number, raw } = node; const nipInfo = getNIPInfo(number); diff --git a/src/components/nostr/RichText/Relay.tsx b/src/components/nostr/RichText/Relay.tsx index 6e2d31c..2a9eeae 100644 --- a/src/components/nostr/RichText/Relay.tsx +++ b/src/components/nostr/RichText/Relay.tsx @@ -1,5 +1,5 @@ import type { RelayNode } from "@/lib/relay-transformer"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; interface RelayNodeProps { node: RelayNode; @@ -18,7 +18,7 @@ function formatRelayUrlForDisplay(url: string): string { * Renders a relay URL as a clickable link that opens the relay viewer */ export function Relay({ node }: RelayNodeProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const { url } = node; const displayUrl = formatRelayUrlForDisplay(url); diff --git a/src/components/nostr/kinds/BaseEventRenderer.tsx b/src/components/nostr/kinds/BaseEventRenderer.tsx index 4b31163..3fea7ce 100644 --- a/src/components/nostr/kinds/BaseEventRenderer.tsx +++ b/src/components/nostr/kinds/BaseEventRenderer.tsx @@ -25,7 +25,7 @@ import { MessageSquare, SmilePlus, } from "lucide-react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow, useGrimoire } from "@/core/state"; import { useCopy } from "@/hooks/useCopy"; import { useAccount } from "@/hooks/useAccount"; import { useSettings } from "@/hooks/useSettings"; @@ -133,7 +133,7 @@ export function EventMenu({ onReactClick?: () => void; canSign?: boolean; }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const { copy, copied } = useCopy(); const [jsonDialogOpen, setJsonDialogOpen] = useState(false); @@ -303,7 +303,7 @@ export function EventContextMenu({ onReactClick?: () => void; canSign?: boolean; }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const { copy, copied } = useCopy(); const [jsonDialogOpen, setJsonDialogOpen] = useState(false); @@ -472,7 +472,7 @@ export function ClickableEventTitle({ className, as: Component = "h3", }: ClickableEventTitleProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); @@ -530,7 +530,8 @@ export function BaseEventContainer({ label?: string; }; }) { - const { locale, addWindow } = useGrimoire(); + const { locale } = useGrimoire(); + const addWindow = useAddWindow(); const { canSign, signer, pubkey } = useAccount(); const { settings } = useSettings(); const [emojiPickerOpen, setEmojiPickerOpen] = useState(false); diff --git a/src/components/nostr/kinds/BlossomServerListRenderer.tsx b/src/components/nostr/kinds/BlossomServerListRenderer.tsx index ddfb84e..3cf5ac9 100644 --- a/src/components/nostr/kinds/BlossomServerListRenderer.tsx +++ b/src/components/nostr/kinds/BlossomServerListRenderer.tsx @@ -1,7 +1,7 @@ import { BaseEventProps, BaseEventContainer } from "./BaseEventRenderer"; import { NostrEvent } from "@/types/nostr"; import { getServersFromEvent } from "@/services/blossom"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { HardDrive, ExternalLink } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -10,7 +10,7 @@ import { Button } from "@/components/ui/button"; * Shows the user's configured Blossom blob storage servers */ export function BlossomServerListRenderer({ event }: BaseEventProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const servers = getServersFromEvent(event); const handleServerClick = (serverUrl: string) => { @@ -73,7 +73,7 @@ export function BlossomServerListDetailRenderer({ }: { event: NostrEvent; }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const servers = getServersFromEvent(event); const handleServerClick = (serverUrl: string) => { diff --git a/src/components/nostr/kinds/ChatMessageRenderer.tsx b/src/components/nostr/kinds/ChatMessageRenderer.tsx index bf7c70f..d3c2381 100644 --- a/src/components/nostr/kinds/ChatMessageRenderer.tsx +++ b/src/components/nostr/kinds/ChatMessageRenderer.tsx @@ -3,7 +3,7 @@ import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { UserName } from "../UserName"; import { MessageCircle } from "lucide-react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { getTagValues } from "@/lib/nostr-utils"; import { isValidHexEventId } from "@/lib/nostr-validation"; import { InlineReplySkeleton } from "@/components/ui/skeleton"; @@ -13,7 +13,7 @@ import { InlineReplySkeleton } from "@/components/ui/skeleton"; * Displays chat messages with optional quoted parent message */ export function Kind9Renderer({ event, depth = 0 }: BaseEventProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); // Parse 'q' tag for quoted parent message const quotedEventIds = getTagValues(event, "q"); diff --git a/src/components/nostr/kinds/CodeSnippetDetailRenderer.tsx b/src/components/nostr/kinds/CodeSnippetDetailRenderer.tsx index ef1ba60..86e9be1 100644 --- a/src/components/nostr/kinds/CodeSnippetDetailRenderer.tsx +++ b/src/components/nostr/kinds/CodeSnippetDetailRenderer.tsx @@ -4,7 +4,7 @@ import { ExternalLink } from "lucide-react"; import { CodeCopyButton } from "@/components/CodeCopyButton"; import { SyntaxHighlight } from "@/components/SyntaxHighlight"; import { useCopy } from "@/hooks/useCopy"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { getCodeLanguage, @@ -32,7 +32,7 @@ interface Kind1337DetailRendererProps { * Note: NIP-C0 helpers wrap getTagValue which caches internally */ export function Kind1337DetailRenderer({ event }: Kind1337DetailRendererProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const { copy, copied } = useCopy(); // All these helpers wrap getTagValue, which caches internally diff --git a/src/components/nostr/kinds/GroupMetadataRenderer.tsx b/src/components/nostr/kinds/GroupMetadataRenderer.tsx index 461d8f6..337818c 100644 --- a/src/components/nostr/kinds/GroupMetadataRenderer.tsx +++ b/src/components/nostr/kinds/GroupMetadataRenderer.tsx @@ -2,7 +2,7 @@ import type { NostrEvent } from "@/types/nostr"; import { getTagValue } from "applesauce-core/helpers"; import { getSeenRelays } from "applesauce-core/helpers/relays"; import { BaseEventContainer, ClickableEventTitle } from "./BaseEventRenderer"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { MessageSquare } from "lucide-react"; interface GroupMetadataRendererProps { @@ -14,7 +14,7 @@ interface GroupMetadataRendererProps { * Displays group info and links to chat */ export function GroupMetadataRenderer({ event }: GroupMetadataRendererProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); // Extract group metadata const groupId = getTagValue(event, "d") || ""; diff --git a/src/components/nostr/kinds/HandlerRecommendationDetailRenderer.tsx b/src/components/nostr/kinds/HandlerRecommendationDetailRenderer.tsx index fd6d673..c2a0df0 100644 --- a/src/components/nostr/kinds/HandlerRecommendationDetailRenderer.tsx +++ b/src/components/nostr/kinds/HandlerRecommendationDetailRenderer.tsx @@ -11,7 +11,7 @@ import { import { KindBadge } from "@/components/KindBadge"; import { Badge } from "@/components/ui/badge"; import { useNostrEvent } from "@/hooks/useNostrEvent"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { UserName } from "../UserName"; import { Globe, Smartphone, TabletSmartphone, Package } from "lucide-react"; import { useState } from "react"; @@ -49,7 +49,7 @@ function HandlerCard({ address: { kind: number; pubkey: string; identifier: string }; platform?: string; }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const handlerEvent = useNostrEvent(address); if (!handlerEvent) { diff --git a/src/components/nostr/kinds/HandlerRecommendationRenderer.tsx b/src/components/nostr/kinds/HandlerRecommendationRenderer.tsx index 3c0a6b9..0017374 100644 --- a/src/components/nostr/kinds/HandlerRecommendationRenderer.tsx +++ b/src/components/nostr/kinds/HandlerRecommendationRenderer.tsx @@ -11,7 +11,7 @@ import { import { KindBadge } from "@/components/KindBadge"; import { Badge } from "@/components/ui/badge"; import { useNostrEvent } from "@/hooks/useNostrEvent"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { Globe, Smartphone, TabletSmartphone, Package } from "lucide-react"; /** @@ -44,7 +44,7 @@ function HandlerItem({ platform?: string; relayHint?: string; }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const handlerEvent = useNostrEvent(address); const appName = handlerEvent ? getAppName(handlerEvent) diff --git a/src/components/nostr/kinds/HighlightDetailRenderer.tsx b/src/components/nostr/kinds/HighlightDetailRenderer.tsx index b0a08e0..99c97c1 100644 --- a/src/components/nostr/kinds/HighlightDetailRenderer.tsx +++ b/src/components/nostr/kinds/HighlightDetailRenderer.tsx @@ -10,7 +10,7 @@ import { } from "applesauce-common/helpers/highlight"; import { EmbeddedEvent } from "../EmbeddedEvent"; import { UserName } from "../UserName"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { formatTimestamp } from "@/hooks/useLocale"; import { RichText } from "../RichText"; @@ -20,7 +20,7 @@ import { RichText } from "../RichText"; * Note: All applesauce helpers cache internally, no useMemo needed */ export function Kind9802DetailRenderer({ event }: { event: NostrEvent }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const highlightText = getHighlightText(event); const comment = getHighlightComment(event); const context = getHighlightContext(event); diff --git a/src/components/nostr/kinds/HighlightRenderer.tsx b/src/components/nostr/kinds/HighlightRenderer.tsx index 8f143fe..03c0c0f 100644 --- a/src/components/nostr/kinds/HighlightRenderer.tsx +++ b/src/components/nostr/kinds/HighlightRenderer.tsx @@ -9,7 +9,7 @@ import { } from "applesauce-common/helpers/highlight"; import { UserName } from "../UserName"; import { useNostrEvent } from "@/hooks/useNostrEvent"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { RichText } from "../RichText"; import { getArticleTitle } from "applesauce-common/helpers/article"; import { KindBadge } from "@/components/KindBadge"; @@ -20,7 +20,7 @@ import { KindBadge } from "@/components/KindBadge"; * Note: All applesauce helpers cache internally, no useMemo needed */ export function Kind9802Renderer({ event }: BaseEventProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const highlightText = getHighlightText(event); const sourceUrl = getHighlightSourceUrl(event); const comment = getHighlightComment(event); diff --git a/src/components/nostr/kinds/IssueStatusDetailRenderer.tsx b/src/components/nostr/kinds/IssueStatusDetailRenderer.tsx index 06b6b52..c1aa641 100644 --- a/src/components/nostr/kinds/IssueStatusDetailRenderer.tsx +++ b/src/components/nostr/kinds/IssueStatusDetailRenderer.tsx @@ -5,7 +5,7 @@ import { UserName } from "../UserName"; import { MarkdownContent } from "../MarkdownContent"; import { EmbeddedEvent } from "../EmbeddedEvent"; import { RepositoryLink } from "../RepositoryLink"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { formatTimestamp } from "@/hooks/useLocale"; import { getStatusRootEventId, @@ -57,7 +57,7 @@ function getStatusBadgeClasses(kind: number): string { * Full view with status info, referenced event, and optional comment */ export function IssueStatusDetailRenderer({ event }: { event: NostrEvent }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const rootEventId = getStatusRootEventId(event); const relayHint = getStatusRootRelayHint(event); diff --git a/src/components/nostr/kinds/IssueStatusRenderer.tsx b/src/components/nostr/kinds/IssueStatusRenderer.tsx index 40e1aeb..95ee291 100644 --- a/src/components/nostr/kinds/IssueStatusRenderer.tsx +++ b/src/components/nostr/kinds/IssueStatusRenderer.tsx @@ -6,7 +6,7 @@ import { import { EmbeddedEvent } from "../EmbeddedEvent"; import { StatusIndicator } from "../StatusIndicator"; import { MarkdownContent } from "../MarkdownContent"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { getStatusRootEventId, getStatusRootRelayHint, @@ -18,7 +18,7 @@ import type { EventPointer } from "nostr-tools/nip19"; * Displays status action with embedded reference to the issue/patch/PR */ export function IssueStatusRenderer({ event }: BaseEventProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const rootEventId = getStatusRootEventId(event); const relayHint = getStatusRootRelayHint(event); diff --git a/src/components/nostr/kinds/Kind1111Renderer.tsx b/src/components/nostr/kinds/Kind1111Renderer.tsx index 3fba028..8a41d21 100644 --- a/src/components/nostr/kinds/Kind1111Renderer.tsx +++ b/src/components/nostr/kinds/Kind1111Renderer.tsx @@ -10,7 +10,7 @@ import { import { useNostrEvent } from "@/hooks/useNostrEvent"; import { UserName } from "../UserName"; import { Reply } from "lucide-react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { InlineReplySkeleton } from "@/components/ui/skeleton"; import { KindBadge } from "@/components/KindBadge"; import { getEventDisplayTitle } from "@/lib/event-title"; @@ -145,7 +145,7 @@ function RootScopeDisplay({ root: CommentRootScope; event: NostrEvent; }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const pointer = scopeToPointer(root.scope); const rootEvent = useNostrEvent(pointer, event); @@ -189,7 +189,7 @@ function RootScopeDisplay({ * Shows root scope (what the thread is about) and parent reply (if nested) */ export function Kind1111Renderer({ event, depth = 0 }: BaseEventProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const root = getCommentRootScope(event); const topLevel = isTopLevelComment(event); diff --git a/src/components/nostr/kinds/LiveChatMessageRenderer.tsx b/src/components/nostr/kinds/LiveChatMessageRenderer.tsx index fe98098..37200ce 100644 --- a/src/components/nostr/kinds/LiveChatMessageRenderer.tsx +++ b/src/components/nostr/kinds/LiveChatMessageRenderer.tsx @@ -6,7 +6,7 @@ import { parseReplaceableAddress } from "applesauce-core/helpers/pointers"; import { getDisplayName } from "@/lib/nostr-utils"; import { useProfile } from "@/hooks/useProfile"; import { Video } from "lucide-react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; /** * Renderer for Kind 1311 - Live Chat Message (NIP-53) @@ -14,7 +14,7 @@ import { useGrimoire } from "@/core/state"; * and a link to the original live activity */ export function LiveChatMessageRenderer({ event, depth = 0 }: BaseEventProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); // Get the 'a' tag pointing to the live activity (kind 30311) const aTag = event.tags.find((tag) => tag[0] === "a"); diff --git a/src/components/nostr/kinds/NoteRenderer.tsx b/src/components/nostr/kinds/NoteRenderer.tsx index 9614de7..9610562 100644 --- a/src/components/nostr/kinds/NoteRenderer.tsx +++ b/src/components/nostr/kinds/NoteRenderer.tsx @@ -5,7 +5,7 @@ import { getNip10References } from "applesauce-common/helpers/threading"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { UserName } from "../UserName"; import { Reply } from "lucide-react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { InlineReplySkeleton } from "@/components/ui/skeleton"; import { KindBadge } from "@/components/KindBadge"; import { getEventDisplayTitle } from "@/lib/event-title"; @@ -72,7 +72,7 @@ function ParentEventCard({ * Shows immediate parent (reply) only for cleaner display */ export function Kind1Renderer({ event, depth = 0 }: BaseEventProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); // Use NIP-10 threading helpers const refs = getNip10References(event); diff --git a/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx b/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx index 8e70ffb..94e1a0c 100644 --- a/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx +++ b/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx @@ -8,7 +8,7 @@ import { FolderGit2, } from "lucide-react"; import { useCopy } from "@/hooks/useCopy"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import type { NostrEvent } from "@/types/nostr"; import { @@ -30,7 +30,7 @@ export function RepositoryStateDetailRenderer({ }: { event: NostrEvent; }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const repoId = useMemo(() => getRepositoryIdentifier(event), [event]); const headRef = useMemo(() => getRepositoryStateHead(event), [event]); const branch = useMemo(() => parseHeadBranch(headRef), [headRef]); diff --git a/src/components/nostr/kinds/RepostRenderer.tsx b/src/components/nostr/kinds/RepostRenderer.tsx index 3111452..b53e120 100644 --- a/src/components/nostr/kinds/RepostRenderer.tsx +++ b/src/components/nostr/kinds/RepostRenderer.tsx @@ -2,7 +2,7 @@ import { Repeat2 } from "lucide-react"; import { getEventPointerFromETag } from "applesauce-core/helpers/pointers"; import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer"; import { EmbeddedEvent } from "../EmbeddedEvent"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; /** * Renderer for Kind 6 (Repost) and Kind 16 (Generic Repost) @@ -12,7 +12,7 @@ import { useGrimoire } from "@/core/state"; * Kind 16: Generic repost for any event kind (NIP-18) */ export function RepostRenderer({ event }: BaseEventProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); // Get the event being reposted (e tag) with relay hints const eTag = event.tags.find((tag) => tag[0] === "e"); diff --git a/src/components/nostr/kinds/SpellRenderer.tsx b/src/components/nostr/kinds/SpellRenderer.tsx index 0002046..63e7024 100644 --- a/src/components/nostr/kinds/SpellRenderer.tsx +++ b/src/components/nostr/kinds/SpellRenderer.tsx @@ -12,7 +12,7 @@ import { CopyableJsonViewer } from "@/components/JsonViewer"; import { User, Users } from "lucide-react"; import { cn } from "@/lib/utils"; import { UserName } from "../UserName"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow, useGrimoire } from "@/core/state"; import { useProfile } from "@/hooks/useProfile"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { getDisplayName } from "@/lib/nostr-utils"; @@ -29,7 +29,7 @@ export function MePlaceholder({ className?: string; pubkey?: string; }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const profile = useProfile(pubkey); const displayName = pubkey ? getDisplayName(pubkey, profile) : "$me"; @@ -67,7 +67,7 @@ export function ContactsPlaceholder({ className?: string; pubkey?: string; }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const contactList = useNostrEvent( pubkey ? { diff --git a/src/components/nostr/kinds/VoiceMessageRenderer.tsx b/src/components/nostr/kinds/VoiceMessageRenderer.tsx index 7f94596..83867fe 100644 --- a/src/components/nostr/kinds/VoiceMessageRenderer.tsx +++ b/src/components/nostr/kinds/VoiceMessageRenderer.tsx @@ -1,7 +1,7 @@ import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer"; import { MediaEmbed } from "../MediaEmbed"; import { useNostrEvent } from "@/hooks/useNostrEvent"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { UserName } from "../UserName"; import { RichText } from "../RichText"; import { InlineReplySkeleton } from "@/components/ui/skeleton"; @@ -68,7 +68,7 @@ function ParentPreview({ * Simple display: just the audio player, with reply context for 1244 */ export function VoiceMessageRenderer({ event }: BaseEventProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); // Audio URL is in event.content per NIP-A0 const audioUrl = event.content.trim(); diff --git a/src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx b/src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx index 5b66c5d..d7570eb 100644 --- a/src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx +++ b/src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx @@ -17,7 +17,7 @@ import { ExternalLink } from "@/components/ExternalLink"; import { MediaEmbed } from "../MediaEmbed"; import { Badge } from "@/components/ui/badge"; import { useMemo } from "react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { Package, Globe, @@ -40,7 +40,7 @@ interface ZapstoreAppDetailRendererProps { * Release item component showing version and download link */ function ReleaseItem({ release }: { release: NostrEvent }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const version = getReleaseVersion(release); const fileEventId = getReleaseFileEventId(release); @@ -158,7 +158,7 @@ function PlatformItem({ platform }: { platform: Platform }) { export function ZapstoreAppDetailRenderer({ event, }: ZapstoreAppDetailRendererProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const appName = getAppName(event); const summary = getAppSummary(event); const iconUrl = getAppIcon(event); diff --git a/src/components/nostr/kinds/ZapstoreAppRenderer.tsx b/src/components/nostr/kinds/ZapstoreAppRenderer.tsx index dcfccf3..6dcf9b8 100644 --- a/src/components/nostr/kinds/ZapstoreAppRenderer.tsx +++ b/src/components/nostr/kinds/ZapstoreAppRenderer.tsx @@ -13,7 +13,7 @@ import { } from "@/lib/zapstore-helpers"; import { PlatformIcon } from "./zapstore/PlatformIcon"; import { useMemo } from "react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { FileDown } from "lucide-react"; import { getSeenRelays } from "applesauce-core/helpers/relays"; import { relayListCache } from "@/services/relay-list-cache"; @@ -25,7 +25,7 @@ import { useLiveTimeline } from "@/hooks/useLiveTimeline"; * Clean feed view with app name, summary, platform icons, and download button */ export function ZapstoreAppRenderer({ event }: BaseEventProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const appName = getAppName(event); const summary = getAppSummary(event); const identifier = getAppIdentifier(event); diff --git a/src/components/nostr/kinds/ZapstoreAppSetDetailRenderer.tsx b/src/components/nostr/kinds/ZapstoreAppSetDetailRenderer.tsx index 7d4e6e5..bee22af 100644 --- a/src/components/nostr/kinds/ZapstoreAppSetDetailRenderer.tsx +++ b/src/components/nostr/kinds/ZapstoreAppSetDetailRenderer.tsx @@ -9,7 +9,7 @@ import { getCurationSetIdentifier, } from "@/lib/zapstore-helpers"; import { useNostrEvent } from "@/hooks/useNostrEvent"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { UserName } from "../UserName"; import { Package } from "lucide-react"; import { PlatformIcon } from "./zapstore/PlatformIcon"; @@ -26,7 +26,7 @@ function AppCard({ }: { address: { kind: number; pubkey: string; identifier: string }; }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const appEvent = useNostrEvent(address); if (!appEvent) { diff --git a/src/components/nostr/kinds/ZapstoreAppSetRenderer.tsx b/src/components/nostr/kinds/ZapstoreAppSetRenderer.tsx index c911a7b..a9795b6 100644 --- a/src/components/nostr/kinds/ZapstoreAppSetRenderer.tsx +++ b/src/components/nostr/kinds/ZapstoreAppSetRenderer.tsx @@ -9,7 +9,7 @@ import { getAppName, } from "@/lib/zapstore-helpers"; import { useNostrEvent } from "@/hooks/useNostrEvent"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { Package } from "lucide-react"; function AppItem({ @@ -17,7 +17,7 @@ function AppItem({ }: { address: { kind: number; pubkey: string; identifier: string }; }) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const appEvent = useNostrEvent(address); const appName = appEvent ? getAppName(appEvent) diff --git a/src/components/nostr/kinds/ZapstoreReleaseDetailRenderer.tsx b/src/components/nostr/kinds/ZapstoreReleaseDetailRenderer.tsx index 08adb93..d042215 100644 --- a/src/components/nostr/kinds/ZapstoreReleaseDetailRenderer.tsx +++ b/src/components/nostr/kinds/ZapstoreReleaseDetailRenderer.tsx @@ -8,7 +8,7 @@ import { getAppIcon, } from "@/lib/zapstore-helpers"; import { useNostrEvent } from "@/hooks/useNostrEvent"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { Badge } from "@/components/ui/badge"; import { UserName } from "../UserName"; import { @@ -29,7 +29,7 @@ interface ZapstoreReleaseDetailRendererProps { export function ZapstoreReleaseDetailRenderer({ event, }: ZapstoreReleaseDetailRendererProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const version = getReleaseVersion(event); const identifier = getReleaseIdentifier(event); const fileEventId = getReleaseFileEventId(event); diff --git a/src/components/nostr/kinds/ZapstoreReleaseRenderer.tsx b/src/components/nostr/kinds/ZapstoreReleaseRenderer.tsx index e788ec7..afaaee4 100644 --- a/src/components/nostr/kinds/ZapstoreReleaseRenderer.tsx +++ b/src/components/nostr/kinds/ZapstoreReleaseRenderer.tsx @@ -10,7 +10,7 @@ import { getAppName, } from "@/lib/zapstore-helpers"; import { useNostrEvent } from "@/hooks/useNostrEvent"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { Badge } from "@/components/ui/badge"; import { Package, FileDown } from "lucide-react"; @@ -19,7 +19,7 @@ import { Package, FileDown } from "lucide-react"; * Displays release version with links to app and download file */ export function ZapstoreReleaseRenderer({ event }: BaseEventProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const version = getReleaseVersion(event); const fileEventId = getReleaseFileEventId(event); const appPointer = getReleaseAppPointer(event); diff --git a/src/components/nostr/lists/EventRefList.tsx b/src/components/nostr/lists/EventRefList.tsx index 9d78dcd..91fcf88 100644 --- a/src/components/nostr/lists/EventRefList.tsx +++ b/src/components/nostr/lists/EventRefList.tsx @@ -1,5 +1,5 @@ import { FileText, ExternalLink } from "lucide-react"; -import { useGrimoire } from "@/core/state"; +import { useAddWindow } from "@/core/state"; import { cn } from "@/lib/utils"; import { EmbeddedEvent } from "../EmbeddedEvent"; import type { EventPointer, AddressPointer } from "nostr-tools/nip19"; @@ -59,7 +59,7 @@ export function EventRefItem({ eventPointer, addressPointer, }: EventRefItemProps) { - const { addWindow } = useGrimoire(); + const addWindow = useAddWindow(); const handleClick = () => { if (eventPointer) { diff --git a/src/core/state.ts b/src/core/state.ts index 9feea0e..4729f2c 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -136,6 +136,38 @@ const activeGrimoireStateAtom = atom( }, ); +/** + * Lightweight hook that returns only the addWindow callback. + * Does NOT subscribe to state changes — safe for use in deeply nested components + * (e.g., MarkdownContent, NostrMention) that only need to open windows. + */ +export const useAddWindow = () => { + const dispatch = useSetAtom(activeGrimoireStateAtom); + + return useCallback( + ( + appId: AppId, + props: any, + commandString?: string, + customTitle?: string, + spellId?: string, + ) => { + dispatch({ + type: "UPDATE", + updater: (prev) => + Logic.addWindow(prev, { + appId, + props, + commandString, + customTitle, + spellId, + }), + }); + }, + [dispatch], + ); +}; + // The Hook export const useGrimoire = () => { const state = useAtomValue(activeGrimoireStateAtom);