From 9ef5e7ecb443b4eb4ac7577cee428d637af49e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Wed, 10 Dec 2025 15:41:12 +0100 Subject: [PATCH] feat: kind 3's and replies --- src/components/EventDetailViewer.tsx | 3 + .../nostr/LinkPreview/PlainLink.tsx | 7 +- src/components/nostr/RichText.tsx | 4 +- src/components/nostr/RichText/Text.tsx | 4 +- .../nostr/kinds/BaseEventRenderer.tsx | 2 +- src/components/nostr/kinds/Kind1Renderer.tsx | 44 +++++++++- src/components/nostr/kinds/Kind3Renderer.tsx | 87 +++++++++++++++++++ src/components/nostr/kinds/index.tsx | 2 + tsconfig.app.tsbuildinfo | 2 +- 9 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 src/components/nostr/kinds/Kind3Renderer.tsx diff --git a/src/components/EventDetailViewer.tsx b/src/components/EventDetailViewer.tsx index d1c7847..219ad81 100644 --- a/src/components/EventDetailViewer.tsx +++ b/src/components/EventDetailViewer.tsx @@ -3,6 +3,7 @@ import type { EventPointer, AddressPointer } from "nostr-tools/nip19"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { KindRenderer } from "./nostr/kinds"; import { Kind0DetailRenderer } from "./nostr/kinds/Kind0DetailRenderer"; +import { Kind3DetailView } from "./nostr/kinds/Kind3Renderer"; import { Kind30023DetailRenderer } from "./nostr/kinds/Kind30023DetailRenderer"; import { Kind9802DetailRenderer } from "./nostr/kinds/Kind9802DetailRenderer"; import { KindBadge } from "./KindBadge"; @@ -167,6 +168,8 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) {
{event.kind === 0 ? ( + ) : event.kind === 3 ? ( + ) : event.kind === 30023 ? ( ) : event.kind === 9802 ? ( diff --git a/src/components/nostr/LinkPreview/PlainLink.tsx b/src/components/nostr/LinkPreview/PlainLink.tsx index 1932067..a0f439a 100644 --- a/src/components/nostr/LinkPreview/PlainLink.tsx +++ b/src/components/nostr/LinkPreview/PlainLink.tsx @@ -1,5 +1,3 @@ -import { ExternalLink } from "lucide-react"; - interface PlainLinkProps { url: string; } @@ -10,10 +8,9 @@ export function PlainLink({ url }: PlainLinkProps) { href={url} target="_blank" rel="noopener noreferrer" - className="inline-flex items-baseline gap-1 text-muted-foreground underline decoration-dotted hover:text-foreground cursor-crosshair break-all" + className="text-muted-foreground underline decoration-dotted hover:text-foreground cursor-crosshair break-all" > - - {url} + {url} ); } diff --git a/src/components/nostr/RichText.tsx b/src/components/nostr/RichText.tsx index 2541be5..22efc49 100644 --- a/src/components/nostr/RichText.tsx +++ b/src/components/nostr/RichText.tsx @@ -36,7 +36,7 @@ export function RichText({ event, content, className = "" }: RichTextProps) { if (content && !event) { const lines = content.trim().split("\n"); return ( -
+
{lines.map((line, idx) => (
{line || "\u00A0"} @@ -54,7 +54,7 @@ export function RichText({ event, content, className = "" }: RichTextProps) { }; const renderedContent = useRenderedContent(trimmedEvent, contentComponents); return ( -
+
{renderedContent}
); diff --git a/src/components/nostr/RichText/Text.tsx b/src/components/nostr/RichText/Text.tsx index 286ed85..7eaee5e 100644 --- a/src/components/nostr/RichText/Text.tsx +++ b/src/components/nostr/RichText/Text.tsx @@ -12,13 +12,13 @@ function hasRTLCharacters(text: string): boolean { export function Text({ node }: TextNodeProps) { const text = node.value; - + // If no newlines, render as inline span if (!text.includes("\n")) { const isRTL = hasRTLCharacters(text); return {text || "\u00A0"}; } - + // If has newlines, use spans with
tags const lines = text.split("\n"); return ( diff --git a/src/components/nostr/kinds/BaseEventRenderer.tsx b/src/components/nostr/kinds/BaseEventRenderer.tsx index 1eeabf7..bbba9c1 100644 --- a/src/components/nostr/kinds/BaseEventRenderer.tsx +++ b/src/components/nostr/kinds/BaseEventRenderer.tsx @@ -144,7 +144,7 @@ export function BaseEventContainer({ }); return ( -
+
{showTimestamp ? ( diff --git a/src/components/nostr/kinds/Kind1Renderer.tsx b/src/components/nostr/kinds/Kind1Renderer.tsx index 753c696..b33e364 100644 --- a/src/components/nostr/kinds/Kind1Renderer.tsx +++ b/src/components/nostr/kinds/Kind1Renderer.tsx @@ -1,12 +1,54 @@ -import { BaseEventProps, BaseEventContainer } from "./BaseEventRenderer"; import { RichText } from "../RichText"; +import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer"; +import { getNip10References } from "applesauce-core/helpers/threading"; +import { useNostrEvent } from "@/hooks/useNostrEvent"; +import { UserName } from "../UserName"; +import { Reply } from "lucide-react"; +import { useGrimoire } from "@/core/state"; /** * Renderer for Kind 1 - Short Text Note */ export function Kind1Renderer({ event, showTimestamp }: BaseEventProps) { + const { addWindow } = useGrimoire(); + const refs = getNip10References(event); + const hasReply = refs.reply?.e || refs.reply?.a; + + // Fetch parent event if replying + const parentEvent = useNostrEvent( + hasReply ? refs.reply?.e || refs.reply?.a : undefined, + ); + + const handleReplyClick = () => { + if (!parentEvent) return; + + const pointer = refs.reply?.e || refs.reply?.a; + if (pointer) { + addWindow( + "open", + { pointer }, + `Reply to ${parentEvent.pubkey.slice(0, 8)}...`, + ); + } + }; + return ( + {hasReply && parentEvent && ( +
+ +
+ + {parentEvent.content} +
+
+ )}
); diff --git a/src/components/nostr/kinds/Kind3Renderer.tsx b/src/components/nostr/kinds/Kind3Renderer.tsx new file mode 100644 index 0000000..409dd4a --- /dev/null +++ b/src/components/nostr/kinds/Kind3Renderer.tsx @@ -0,0 +1,87 @@ +import { useGrimoire } from "@/core/state"; +import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer"; +import { UserName } from "../UserName"; +import { Users, Sparkles } from "lucide-react"; + +/** + * Kind 3 Renderer - Contact/Follow List + * Shows follow count and "follows you" indicator + */ +export function Kind3Renderer({ event, showTimestamp }: BaseEventProps) { + const { state } = useGrimoire(); + + // Extract followed pubkeys from p tags + const followedPubkeys = event.tags + .filter((tag) => tag[0] === "p") + .map((tag) => tag[1]); + + const followsYou = state.activeAccount?.pubkey + ? followedPubkeys.includes(state.activeAccount.pubkey) + : false; + + return ( + +
+ + + Following {followedPubkeys.length} people + + {followsYou && ( + + + Follows you + + )} +
+
+ ); +} + +/** + * Kind 3 Detail View - Full follow list + * Shows all followed users in order + */ +export function Kind3DetailView({ event }: { event: any }) { + const { state } = useGrimoire(); + + // Extract followed pubkeys from p tags + const followedPubkeys = event.tags + .filter((tag: string[]) => tag[0] === "p" && tag[1].length === 64) + .map((tag: string[]) => tag[1]); + + const followsYou = state.activeAccount?.pubkey + ? followedPubkeys.includes(state.activeAccount.pubkey) + : false; + + return ( +
+
+
+ + Contacts +
+ +
+ Following {followedPubkeys.length} people + {followsYou && ( + + + Follows you + + )} +
+
+ +
+
+ {followedPubkeys.map((pubkey: string) => ( +
+ + +
+ ))} +
+
+
+ ); +} diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx index f5078b9..4440dda 100644 --- a/src/components/nostr/kinds/index.tsx +++ b/src/components/nostr/kinds/index.tsx @@ -1,5 +1,6 @@ import { Kind0Renderer } from "./Kind0Renderer"; import { Kind1Renderer } from "./Kind1Renderer"; +import { Kind3Renderer } from "./Kind3Renderer"; import { Kind6Renderer } from "./Kind6Renderer"; import { Kind7Renderer } from "./Kind7Renderer"; import { Kind20Renderer } from "./Kind20Renderer"; @@ -19,6 +20,7 @@ import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer"; const kindRenderers: Record> = { 0: Kind0Renderer, // Profile Metadata 1: Kind1Renderer, // Short Text Note + 3: Kind3Renderer, // Contact List 6: Kind6Renderer, // Repost 7: Kind7Renderer, // Reaction 20: Kind20Renderer, // Picture (NIP-68) diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo index df12982..a56aa74 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/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/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