mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 15:07:10 +02:00
feat: kind 3's and replies
This commit is contained in:
@@ -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) {
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{event.kind === 0 ? (
|
||||
<Kind0DetailRenderer event={event} />
|
||||
) : event.kind === 3 ? (
|
||||
<Kind3DetailView event={event} />
|
||||
) : event.kind === 30023 ? (
|
||||
<Kind30023DetailRenderer event={event} />
|
||||
) : event.kind === 9802 ? (
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3 flex-shrink-0" />
|
||||
<span>{url}</span>
|
||||
{url}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export function RichText({ event, content, className = "" }: RichTextProps) {
|
||||
if (content && !event) {
|
||||
const lines = content.trim().split("\n");
|
||||
return (
|
||||
<div className={cn("leading-tight break-words", className)}>
|
||||
<div className={cn("leading-relaxed break-words", className)}>
|
||||
{lines.map((line, idx) => (
|
||||
<div key={idx} dir="auto">
|
||||
{line || "\u00A0"}
|
||||
@@ -54,7 +54,7 @@ export function RichText({ event, content, className = "" }: RichTextProps) {
|
||||
};
|
||||
const renderedContent = useRenderedContent(trimmedEvent, contentComponents);
|
||||
return (
|
||||
<div className={cn("leading-tight break-words", className)}>
|
||||
<div className={cn("leading-relaxed break-words", className)}>
|
||||
{renderedContent}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 <span dir={isRTL ? "rtl" : "auto"}>{text || "\u00A0"}</span>;
|
||||
}
|
||||
|
||||
|
||||
// If has newlines, use spans with <br> tags
|
||||
const lines = text.split("\n");
|
||||
return (
|
||||
|
||||
@@ -144,7 +144,7 @@ export function BaseEventContainer({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 p-2">
|
||||
<div className="flex flex-col gap-2 p-3 border-b border-border/50 last:border-0">
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<EventAuthor pubkey={event.pubkey} />
|
||||
{showTimestamp ? (
|
||||
|
||||
@@ -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 (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
{hasReply && parentEvent && (
|
||||
<div
|
||||
onClick={handleReplyClick}
|
||||
className="flex items-start gap-2 p-1 bg-muted/20 text-xs text-muted-foreground hover:bg-muted/30 cursor-pointer rounded transition-colors"
|
||||
>
|
||||
<Reply className="size-3 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex items-baseline gap-1 min-w-0 flex-1">
|
||||
<UserName
|
||||
pubkey={parentEvent.pubkey}
|
||||
className="flex-shrink-0 text-accent"
|
||||
/>
|
||||
<span className="truncate">{parentEvent.content}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<RichText event={event} className="text-sm" />
|
||||
</BaseEventContainer>
|
||||
);
|
||||
|
||||
87
src/components/nostr/kinds/Kind3Renderer.tsx
Normal file
87
src/components/nostr/kinds/Kind3Renderer.tsx
Normal file
@@ -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 (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<div className="flex flex-col gap-2 text-xs">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users className="size-3 text-muted-foreground" />
|
||||
Following {followedPubkeys.length} people
|
||||
</span>
|
||||
{followsYou && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Sparkles className="size-3 text-muted-foreground" />
|
||||
Follows you
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</BaseEventContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="size-5" />
|
||||
<span className="text-lg font-semibold">Contacts</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 text-sm">
|
||||
<span>Following {followedPubkeys.length} people</span>
|
||||
{followsYou && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Sparkles className="size-4 text-muted-foreground" />
|
||||
Follows you
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
{followedPubkeys.map((pubkey: string) => (
|
||||
<div key={pubkey} className="flex items-center gap-2 text-sm">
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<UserName pubkey={pubkey} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<number, React.ComponentType<BaseEventProps>> = {
|
||||
0: Kind0Renderer, // Profile Metadata
|
||||
1: Kind1Renderer, // Short Text Note
|
||||
3: Kind3Renderer, // Contact List
|
||||
6: Kind6Renderer, // Repost
|
||||
7: Kind7Renderer, // Reaction
|
||||
20: Kind20Renderer, // Picture (NIP-68)
|
||||
|
||||
@@ -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"}
|
||||
{"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"}
|
||||
Reference in New Issue
Block a user