diff --git a/src/components/nostr/QuotedEvent.tsx b/src/components/nostr/QuotedEvent.tsx
new file mode 100644
index 0000000..1e8b8f0
--- /dev/null
+++ b/src/components/nostr/QuotedEvent.tsx
@@ -0,0 +1,123 @@
+import { useState } from "react";
+import { useNostrEvent } from "@/hooks/useNostrEvent";
+import { KindRenderer } from "./kinds";
+import { UserName } from "./UserName";
+import { ChevronDown, ChevronUp } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+interface QuotedEventProps {
+ /** Event ID string for regular events */
+ eventId?: string;
+ /** AddressPointer for addressable/replaceable events */
+ addressPointer?: { kind: number; pubkey: string; identifier: string };
+ /** Callback when user clicks to open the event in new window */
+ onOpen?: (
+ id: string | { kind: number; pubkey: string; identifier: string },
+ ) => void;
+ /** Depth level for nesting (0 = root, 1 = first quote, 2+ = nested) */
+ depth?: number;
+ /** Optional className for container */
+ className?: string;
+}
+
+/**
+ * QuotedEvent component with depth-aware rendering
+ * - depth 0-1: Show full content inline by default
+ * - depth 2+: Show expandable preview only
+ */
+export function QuotedEvent({
+ eventId,
+ addressPointer,
+ onOpen,
+ depth = 1,
+ className,
+}: QuotedEventProps) {
+ const [isExpanded, setIsExpanded] = useState(depth < 2);
+
+ // Determine pointer to use
+ const pointer = eventId || addressPointer;
+
+ // Load the event
+ const event = useNostrEvent(pointer);
+
+ // Loading state
+ if (!event) {
+ if (onOpen && pointer) {
+ const displayText =
+ typeof eventId === "string"
+ ? `@${eventId.slice(0, 8)}...`
+ : addressPointer
+ ? `@${addressPointer.identifier || addressPointer.kind}`
+ : "@event";
+
+ return (
+ {
+ e.preventDefault();
+ onOpen(pointer);
+ }}
+ className="inline-flex items-center gap-1 text-accent underline decoration-dotted break-all"
+ >
+ {displayText}
+
+ );
+ }
+
+ return (
+
+ Loading event...
+
+ );
+ }
+
+ // For depth 0-1: Show full content inline by default
+ if (depth < 2) {
+ return (
+
+
+
+ );
+ }
+
+ // For depth 2+: Show expandable preview
+ const previewText = event.content?.slice(0, 100) || "";
+ const hasMore = event.content?.length > 100;
+
+ return (
+
+ {/* Preview header - always visible */}
+
+
+ {/* Full content - shown when expanded */}
+ {isExpanded && (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/nostr/RichText.tsx b/src/components/nostr/RichText.tsx
index 22efc49..abe7c84 100644
--- a/src/components/nostr/RichText.tsx
+++ b/src/components/nostr/RichText.tsx
@@ -1,5 +1,6 @@
import { cn } from "@/lib/utils";
import { Hooks } from "applesauce-react";
+import { createContext, useContext } from "react";
import { Text } from "./RichText/Text";
import { Hashtag } from "./RichText/Hashtag";
import { Mention } from "./RichText/Mention";
@@ -10,10 +11,18 @@ import type { NostrEvent } from "@/types/nostr";
const { useRenderedContent } = Hooks;
+// Context for passing depth through RichText rendering
+const DepthContext = createContext(1);
+
+export function useDepth() {
+ return useContext(DepthContext);
+}
+
interface RichTextProps {
event?: NostrEvent;
content?: string;
className?: string;
+ depth?: number;
}
// Content node component types for rendering
@@ -31,7 +40,12 @@ const contentComponents = {
* Supports mentions, hashtags, links, emojis, and galleries
* Can also render plain text without requiring a full event
*/
-export function RichText({ event, content, className = "" }: RichTextProps) {
+export function RichText({
+ event,
+ content,
+ className = "",
+ depth = 1,
+}: RichTextProps) {
// If plain content is provided, just render it
if (content && !event) {
const lines = content.trim().split("\n");
@@ -54,9 +68,11 @@ export function RichText({ event, content, className = "" }: RichTextProps) {
};
const renderedContent = useRenderedContent(trimmedEvent, contentComponents);
return (
-
- {renderedContent}
-
+
+
+ {renderedContent}
+
+
);
}
diff --git a/src/components/nostr/RichText/EventEmbed.tsx b/src/components/nostr/RichText/EventEmbed.tsx
index 4b5fa28..1742b5d 100644
--- a/src/components/nostr/RichText/EventEmbed.tsx
+++ b/src/components/nostr/RichText/EventEmbed.tsx
@@ -1,58 +1,27 @@
-import { useState } from "react";
import { EventPointer, AddressPointer } from "nostr-tools/nip19";
-import { Plus, Minus } from "lucide-react";
-import { EmbeddedEvent } from "../EmbeddedEvent";
+import { QuotedEvent } from "../QuotedEvent";
interface EventEmbedNodeProps {
node: {
pointer: EventPointer | AddressPointer;
};
+ depth?: number;
}
-function isEventPointer(
- pointer: EventPointer | AddressPointer,
-): pointer is EventPointer {
- return "id" in pointer;
-}
-
-export function EventEmbed({ node }: EventEmbedNodeProps) {
- const [isExpanded, setIsExpanded] = useState(false);
+/**
+ * EventEmbed component for rendering quoted/embedded Nostr events
+ * Uses QuotedEvent with depth tracking for smart expand/collapse behavior
+ */
+export function EventEmbed({ node, depth = 1 }: EventEmbedNodeProps) {
const { pointer } = node;
- // Determine the type label and short identifier
- const isEvent = isEventPointer(pointer);
- const label = isEvent ? "nevent" : "naddr";
- const identifier = isEvent
- ? pointer.id.slice(0, 8)
- : pointer.identifier || pointer.pubkey.slice(0, 8);
-
return (
-
-
-
- {isExpanded && (
-
- )}
-
+
);
}
diff --git a/src/components/nostr/RichText/Mention.tsx b/src/components/nostr/RichText/Mention.tsx
index fc98c80..17fedcd 100644
--- a/src/components/nostr/RichText/Mention.tsx
+++ b/src/components/nostr/RichText/Mention.tsx
@@ -2,6 +2,7 @@ import { kinds } from "nostr-tools";
import { UserName } from "../UserName";
import { EventEmbed } from "./EventEmbed";
import { EventPointer, AddressPointer } from "nostr-tools/nip19";
+import { useDepth } from "../RichText";
interface MentionNodeProps {
node: {
@@ -14,6 +15,8 @@ interface MentionNodeProps {
}
export function Mention({ node }: MentionNodeProps) {
+ const depth = useDepth();
+
if (node.decoded?.type === "npub") {
const pubkey = node.decoded.data;
return (
@@ -43,17 +46,17 @@ export function Mention({ node }: MentionNodeProps) {
kind: kinds.ShortTextNote,
relays: [],
};
- return ;
+ return ;
}
if (node.decoded?.type === "nevent") {
const pointer: EventPointer = node.decoded.data;
- return ;
+ return ;
}
if (node.decoded?.type === "naddr") {
const pointer: AddressPointer = node.decoded.data;
- return ;
+ return ;
}
return null;
diff --git a/src/components/nostr/kinds/BaseEventRenderer.tsx b/src/components/nostr/kinds/BaseEventRenderer.tsx
index bbba9c1..ffd2478 100644
--- a/src/components/nostr/kinds/BaseEventRenderer.tsx
+++ b/src/components/nostr/kinds/BaseEventRenderer.tsx
@@ -20,6 +20,7 @@ import { JsonViewer } from "@/components/JsonViewer";
export interface BaseEventProps {
event: NostrEvent;
showTimestamp?: boolean;
+ depth?: number;
}
/**
diff --git a/src/components/nostr/kinds/Kind1Renderer.tsx b/src/components/nostr/kinds/Kind1Renderer.tsx
index b33e364..cce4711 100644
--- a/src/components/nostr/kinds/Kind1Renderer.tsx
+++ b/src/components/nostr/kinds/Kind1Renderer.tsx
@@ -9,7 +9,11 @@ import { useGrimoire } from "@/core/state";
/**
* Renderer for Kind 1 - Short Text Note
*/
-export function Kind1Renderer({ event, showTimestamp }: BaseEventProps) {
+export function Kind1Renderer({
+ event,
+ showTimestamp,
+ depth = 0,
+}: BaseEventProps) {
const { addWindow } = useGrimoire();
const refs = getNip10References(event);
const hasReply = refs.reply?.e || refs.reply?.a;
@@ -49,7 +53,7 @@ export function Kind1Renderer({ event, showTimestamp }: BaseEventProps) {
)}
-
+
);
}
diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx
index 4440dda..caba42f 100644
--- a/src/components/nostr/kinds/index.tsx
+++ b/src/components/nostr/kinds/index.tsx
@@ -57,12 +57,14 @@ function DefaultKindRenderer({ event, showTimestamp }: BaseEventProps) {
export function KindRenderer({
event,
showTimestamp = false,
+ depth = 0,
}: {
event: NostrEvent;
showTimestamp?: boolean;
+ depth?: number;
}) {
const Renderer = kindRenderers[event.kind] || DefaultKindRenderer;
- return ;
+ return ;
}
/**
diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo
index a56aa74..cee3c77 100644
--- a/tsconfig.app.tsbuildinfo
+++ b/tsconfig.app.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.tsx","./src/root.tsx","./src/vite-env.d.ts","./src/components/command.tsx","./src/components/commandlauncher.tsx","./src/components/decodeviewer.tsx","./src/components/encodeviewer.tsx","./src/components/eventdetailviewer.tsx","./src/components/grimoirewelcome.tsx","./src/components/home.tsx","./src/components/jsonviewer.tsx","./src/components/kindbadge.tsx","./src/components/kindrenderer.tsx","./src/components/manpage.tsx","./src/components/markdown.tsx","./src/components/niprenderer.tsx","./src/components/profileviewer.tsx","./src/components/reqviewer.tsx","./src/components/tabbar.tsx","./src/components/timestamp.tsx","./src/components/winviewer.tsx","./src/components/windowtoolbar.tsx","./src/components/nostr/embeddedevent.tsx","./src/components/nostr/feed.tsx","./src/components/nostr/mediadialog.tsx","./src/components/nostr/mediaembed.tsx","./src/components/nostr/richtext.tsx","./src/components/nostr/username.tsx","./src/components/nostr/index.ts","./src/components/nostr/nip05.tsx","./src/components/nostr/npub.tsx","./src/components/nostr/relay-pool.tsx","./src/components/nostr/user-menu.tsx","./src/components/nostr/linkpreview/audiolink.tsx","./src/components/nostr/linkpreview/imagelink.tsx","./src/components/nostr/linkpreview/plainlink.tsx","./src/components/nostr/linkpreview/videolink.tsx","./src/components/nostr/linkpreview/index.ts","./src/components/nostr/richtext/emoji.tsx","./src/components/nostr/richtext/eventembed.tsx","./src/components/nostr/richtext/gallery.tsx","./src/components/nostr/richtext/hashtag.tsx","./src/components/nostr/richtext/link.tsx","./src/components/nostr/richtext/mention.tsx","./src/components/nostr/richtext/text.tsx","./src/components/nostr/richtext/index.ts","./src/components/nostr/kinds/baseeventrenderer.tsx","./src/components/nostr/kinds/kind0detailrenderer.tsx","./src/components/nostr/kinds/kind0renderer.tsx","./src/components/nostr/kinds/kind1063renderer.tsx","./src/components/nostr/kinds/kind1renderer.tsx","./src/components/nostr/kinds/kind20renderer.tsx","./src/components/nostr/kinds/kind21renderer.tsx","./src/components/nostr/kinds/kind22renderer.tsx","./src/components/nostr/kinds/kind30023detailrenderer.tsx","./src/components/nostr/kinds/kind30023renderer.tsx","./src/components/nostr/kinds/kind3renderer.tsx","./src/components/nostr/kinds/kind6renderer.tsx","./src/components/nostr/kinds/kind7renderer.tsx","./src/components/nostr/kinds/kind9735renderer.tsx","./src/components/nostr/kinds/kind9802detailrenderer.tsx","./src/components/nostr/kinds/kind9802renderer.tsx","./src/components/nostr/kinds/index.tsx","./src/components/ui/avatar.tsx","./src/components/ui/button.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/constants/kinds.ts","./src/constants/nips.ts","./src/core/logic.ts","./src/core/state.ts","./src/hooks/useaccountsync.ts","./src/hooks/usenip.ts","./src/hooks/usenip05.ts","./src/hooks/usenostrevent.ts","./src/hooks/useprofile.ts","./src/hooks/usereqtimeline.ts","./src/hooks/usetimeline.ts","./src/lib/decode-parser.ts","./src/lib/encode-parser.ts","./src/lib/imeta.ts","./src/lib/nip-kinds.ts","./src/lib/nip05.ts","./src/lib/nostr-utils.ts","./src/lib/open-parser.ts","./src/lib/profile-parser.ts","./src/lib/req-parser.ts","./src/lib/utils.ts","./src/services/accounts.ts","./src/services/db.ts","./src/services/event-store.ts","./src/services/loaders.ts","./src/services/relay-pool.ts","./src/types/app.ts","./src/types/man.ts","./src/types/nostr.ts","./src/types/profile.ts"],"version":"5.6.3"}
\ No newline at end of file
+{"root":["./src/main.tsx","./src/root.tsx","./src/vite-env.d.ts","./src/components/command.tsx","./src/components/commandlauncher.tsx","./src/components/decodeviewer.tsx","./src/components/encodeviewer.tsx","./src/components/eventdetailviewer.tsx","./src/components/grimoirewelcome.tsx","./src/components/home.tsx","./src/components/jsonviewer.tsx","./src/components/kindbadge.tsx","./src/components/kindrenderer.tsx","./src/components/manpage.tsx","./src/components/markdown.tsx","./src/components/niprenderer.tsx","./src/components/profileviewer.tsx","./src/components/reqviewer.tsx","./src/components/tabbar.tsx","./src/components/timestamp.tsx","./src/components/winviewer.tsx","./src/components/windowtoolbar.tsx","./src/components/nostr/embeddedevent.tsx","./src/components/nostr/feed.tsx","./src/components/nostr/mediadialog.tsx","./src/components/nostr/mediaembed.tsx","./src/components/nostr/quotedevent.tsx","./src/components/nostr/richtext.tsx","./src/components/nostr/username.tsx","./src/components/nostr/index.ts","./src/components/nostr/nip05.tsx","./src/components/nostr/npub.tsx","./src/components/nostr/relay-pool.tsx","./src/components/nostr/user-menu.tsx","./src/components/nostr/linkpreview/audiolink.tsx","./src/components/nostr/linkpreview/imagelink.tsx","./src/components/nostr/linkpreview/plainlink.tsx","./src/components/nostr/linkpreview/videolink.tsx","./src/components/nostr/linkpreview/index.ts","./src/components/nostr/richtext/emoji.tsx","./src/components/nostr/richtext/eventembed.tsx","./src/components/nostr/richtext/gallery.tsx","./src/components/nostr/richtext/hashtag.tsx","./src/components/nostr/richtext/link.tsx","./src/components/nostr/richtext/mention.tsx","./src/components/nostr/richtext/text.tsx","./src/components/nostr/richtext/index.ts","./src/components/nostr/kinds/baseeventrenderer.tsx","./src/components/nostr/kinds/kind0detailrenderer.tsx","./src/components/nostr/kinds/kind0renderer.tsx","./src/components/nostr/kinds/kind1063renderer.tsx","./src/components/nostr/kinds/kind1renderer.tsx","./src/components/nostr/kinds/kind20renderer.tsx","./src/components/nostr/kinds/kind21renderer.tsx","./src/components/nostr/kinds/kind22renderer.tsx","./src/components/nostr/kinds/kind30023detailrenderer.tsx","./src/components/nostr/kinds/kind30023renderer.tsx","./src/components/nostr/kinds/kind3renderer.tsx","./src/components/nostr/kinds/kind6renderer.tsx","./src/components/nostr/kinds/kind7renderer.tsx","./src/components/nostr/kinds/kind9735renderer.tsx","./src/components/nostr/kinds/kind9802detailrenderer.tsx","./src/components/nostr/kinds/kind9802renderer.tsx","./src/components/nostr/kinds/index.tsx","./src/components/ui/avatar.tsx","./src/components/ui/button.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/constants/kinds.ts","./src/constants/nips.ts","./src/core/logic.ts","./src/core/state.ts","./src/hooks/useaccountsync.ts","./src/hooks/usenip.ts","./src/hooks/usenip05.ts","./src/hooks/usenostrevent.ts","./src/hooks/useprofile.ts","./src/hooks/usereqtimeline.ts","./src/hooks/usetimeline.ts","./src/lib/decode-parser.ts","./src/lib/encode-parser.ts","./src/lib/imeta.ts","./src/lib/nip-kinds.ts","./src/lib/nip05.ts","./src/lib/nostr-utils.ts","./src/lib/open-parser.ts","./src/lib/profile-parser.ts","./src/lib/req-parser.ts","./src/lib/utils.ts","./src/services/accounts.ts","./src/services/db.ts","./src/services/event-store.ts","./src/services/loaders.ts","./src/services/relay-pool.ts","./src/types/app.ts","./src/types/man.ts","./src/types/nostr.ts","./src/types/profile.ts"],"version":"5.6.3"}
\ No newline at end of file