mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-06 18:51:21 +02:00
fix: avoid unnecesary re-renders
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<string[]>([]);
|
||||
const [newRelay, setNewRelay] = useState("");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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+/);
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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<HTMLInputElement>(null);
|
||||
const { addWindow } = useGrimoire();
|
||||
const addWindow = useAddWindow();
|
||||
|
||||
// Autofocus on mount
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 === "_";
|
||||
|
||||
@@ -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 (
|
||||
<div className="my-4 p-4 border border-border rounded-lg bg-muted/10 text-sm text-muted-foreground">
|
||||
<p>Media unavailable (relative URL without base)</p>
|
||||
<p className="text-xs mt-1 break-all">{src}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<MediaEmbed
|
||||
url={resolvedUrl}
|
||||
alt={alt}
|
||||
preset="preview"
|
||||
enableZoom
|
||||
className="my-4"
|
||||
/>
|
||||
);
|
||||
},
|
||||
// Handle links: nostr mentions, NIP links, and regular URLs
|
||||
a: ({
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
if (!href) return null;
|
||||
|
||||
// Render nostr: mentions inline
|
||||
if (href.startsWith("nostr:")) {
|
||||
return <NostrMention href={href} />;
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<a
|
||||
href={`#nip-${nipNumber}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
addWindow("nip", { number: nipNumber }, `NIP ${nipNumber}`);
|
||||
}}
|
||||
className="text-accent underline decoration-dotted cursor-crosshair hover:text-accent/80"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// No canonical URL and it's relative - can't resolve
|
||||
return null;
|
||||
// Regular links with break-all for long URLs
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className="text-accent underline decoration-dotted break-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
[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<HTMLHeadingElement>) => (
|
||||
<h1 className="text-2xl font-bold mt-8 mb-4" {...props} />
|
||||
),
|
||||
h2: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h2 className="text-xl font-bold mt-6 mb-3" {...props} />
|
||||
),
|
||||
h3: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h3 className="text-lg font-bold mt-4 mb-2" {...props} />
|
||||
),
|
||||
p: (props: React.HTMLAttributes<HTMLParagraphElement>) => (
|
||||
<p className="text-sm leading-relaxed mb-4" {...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 (
|
||||
<code
|
||||
className="bg-muted px-0.5 py-0.5 rounded text-xs font-mono"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
// Block code with syntax highlighting and copy button
|
||||
return <CodeBlock code={code} language={language} />;
|
||||
},
|
||||
blockquote: (props: React.HTMLAttributes<HTMLQuoteElement>) => (
|
||||
<blockquote
|
||||
className="border-l-4 border-muted pl-4 italic text-muted-foreground my-4"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ul: (props: React.HTMLAttributes<HTMLUListElement>) => (
|
||||
<ul
|
||||
className="text-sm list-disc list-inside my-4 space-y-2"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ol: (props: React.HTMLAttributes<HTMLOListElement>) => (
|
||||
<ol
|
||||
className="text-sm list-decimal list-inside my-4 space-y-2"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
hr: () => <hr className="my-4" />,
|
||||
}),
|
||||
[resolveUrl, addWindow],
|
||||
);
|
||||
|
||||
return (
|
||||
<article className="prose prose-invert prose-sm max-w-none">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkNostrMentions]}
|
||||
remarkPlugins={REMARK_PLUGINS}
|
||||
skipHtml
|
||||
urlTransform={(url) => {
|
||||
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 (
|
||||
<div className="my-4 p-4 border border-border rounded-lg bg-muted/10 text-sm text-muted-foreground">
|
||||
<p>Media unavailable (relative URL without base)</p>
|
||||
<p className="text-xs mt-1 break-all">{src}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MediaEmbed
|
||||
url={resolvedUrl}
|
||||
alt={alt}
|
||||
preset="preview"
|
||||
enableZoom
|
||||
className="my-4"
|
||||
/>
|
||||
);
|
||||
},
|
||||
// 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 <NostrMention href={href} />;
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<a
|
||||
href={`#nip-${nipNumber}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
addWindow(
|
||||
"nip",
|
||||
{ number: nipNumber },
|
||||
`NIP ${nipNumber}`,
|
||||
);
|
||||
}}
|
||||
className="text-accent underline decoration-dotted cursor-crosshair hover:text-accent/80"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Regular links with break-all for long URLs
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className="text-accent underline decoration-dotted break-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
// 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 }) => (
|
||||
<h1 className="text-2xl font-bold mt-8 mb-4" {...props} />
|
||||
),
|
||||
h2: ({ ...props }) => (
|
||||
<h2 className="text-xl font-bold mt-6 mb-3" {...props} />
|
||||
),
|
||||
h3: ({ ...props }) => (
|
||||
<h3 className="text-lg font-bold mt-4 mb-2" {...props} />
|
||||
),
|
||||
p: ({ ...props }) => (
|
||||
<p className="text-sm leading-relaxed mb-4" {...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 (
|
||||
<code
|
||||
className="bg-muted px-0.5 py-0.5 rounded text-xs font-mono"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
// Block code with syntax highlighting and copy button
|
||||
return <CodeBlock code={code} language={language} />;
|
||||
},
|
||||
blockquote: ({ ...props }) => (
|
||||
<blockquote
|
||||
className="border-l-4 border-muted pl-4 italic text-muted-foreground my-4"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ul: ({ ...props }) => (
|
||||
<ul
|
||||
className="text-sm list-disc list-inside my-4 space-y-2"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ol: ({ ...props }) => (
|
||||
<ol
|
||||
className="text-sm list-decimal list-inside my-4 space-y-2"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
hr: () => <hr className="my-4" />,
|
||||
}}
|
||||
urlTransform={urlTransform}
|
||||
components={components}
|
||||
>
|
||||
{content.replace(/\\n/g, "\n")}
|
||||
{processedContent}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
);
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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") || "";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
? {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user