Improve NIP-72 community context for comments: show 'Posted in' with community preview
This commit is contained in:
@@ -60,14 +60,14 @@ function getEventDisplayName(event: NostrEvent): string {
|
||||
const title = event.tags.find(([name]) => name === 'title')?.[1];
|
||||
if (title) return title;
|
||||
|
||||
// Try name tag (used by communities, emoji packs, etc.)
|
||||
const name = event.tags.find(([name]) => name === 'name')?.[1];
|
||||
if (name) return name;
|
||||
|
||||
// Try d tag (addressable events use this as an identifier)
|
||||
const dTag = event.tags.find(([name]) => name === 'd')?.[1];
|
||||
if (dTag) return dTag;
|
||||
|
||||
// Try name tag (used by some events)
|
||||
const name = event.tags.find(([name]) => name === 'name')?.[1];
|
||||
if (name) return name;
|
||||
|
||||
// Fall back to kind label from EXTRA_KINDS
|
||||
const kindDef = EXTRA_KINDS.find((def) =>
|
||||
def.subKinds?.some((sub) => sub.kind === event.kind) || def.kind === event.kind,
|
||||
@@ -181,10 +181,13 @@ function ProfileCommentContext({ pubkey, className }: { pubkey: string; classNam
|
||||
function GenericAddrCommentContext({ root, className }: { root: CommentRoot; className?: string }) {
|
||||
const { data: event, isLoading } = useAddrEvent(root.addr);
|
||||
|
||||
const isCommunity = root.rootKind === '34550' || root.addr?.kind === 34550;
|
||||
const prefix = isCommunity ? 'Posted in' : 'Commenting on';
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={className || 'flex items-center gap-x-1 text-sm text-muted-foreground mt-2 mb-1'}>
|
||||
<span className="shrink-0">Commenting on</span>
|
||||
<span className="shrink-0">{prefix}</span>
|
||||
<Skeleton className="h-3.5 w-24 inline-block" />
|
||||
</div>
|
||||
);
|
||||
@@ -195,7 +198,7 @@ function GenericAddrCommentContext({ root, className }: { root: CommentRoot; cla
|
||||
|
||||
return (
|
||||
<div className={className || 'flex items-center gap-x-1 text-sm text-muted-foreground mt-2 mb-1 min-w-0 overflow-hidden'}>
|
||||
<span className="shrink-0">Commenting on</span>
|
||||
<span className="shrink-0">{prefix}</span>
|
||||
{link ? (
|
||||
<Link
|
||||
to={link}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { BookOpen, ExternalLink, Globe, MapPin, User } from 'lucide-react';
|
||||
import { BookOpen, ExternalLink, Globe, MapPin, User, Users } from 'lucide-react';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ExternalFavicon } from '@/components/ExternalFavicon';
|
||||
@@ -8,6 +9,7 @@ import { LinkEmbed } from '@/components/LinkEmbed';
|
||||
import { extractYouTubeId } from '@/lib/linkEmbed';
|
||||
import { useLinkPreview } from '@/hooks/useLinkPreview';
|
||||
import { useBookInfo } from '@/hooks/useBookInfo';
|
||||
import { useAddrEvent } from '@/hooks/useEvent';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useProfileUrl } from '@/hooks/useProfileUrl';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
@@ -361,6 +363,78 @@ function CountryPreview({ code, link }: { code: string; link: string }) {
|
||||
* on its detail page when the root is a kind 0 profile event.
|
||||
* Links to the profile page.
|
||||
*/
|
||||
/**
|
||||
* Compact preview of a NIP-72 community, shown above a kind 1111 comment
|
||||
* on its detail page when the root is a kind 34550 community definition.
|
||||
* Links to the community detail page.
|
||||
*/
|
||||
export function CommunityPreview({ addr }: { addr: { kind: number; pubkey: string; identifier: string } }) {
|
||||
const { data: event, isLoading } = useAddrEvent(addr);
|
||||
|
||||
const communityName = event?.tags.find(([n]) => n === 'name')?.[1]
|
||||
|| event?.tags.find(([n]) => n === 'd')?.[1]
|
||||
|| 'Community';
|
||||
const communityImage = event?.tags.find(([n]) => n === 'image')?.[1];
|
||||
const communityDescription = event?.tags.find(([n]) => n === 'description')?.[1];
|
||||
const moderatorCount = event?.tags.filter(([n, , , role]) => n === 'p' && role === 'moderator').length ?? 0;
|
||||
|
||||
const link = useMemo(() => {
|
||||
return `/${nip19.naddrEncode({ kind: addr.kind, pubkey: addr.pubkey, identifier: addr.identifier })}`;
|
||||
}, [addr]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="px-4 py-3 border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="size-12 rounded-lg shrink-0" />
|
||||
<div className="flex-1 min-w-0 space-y-1.5">
|
||||
<Skeleton className="h-3 w-16" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={link}
|
||||
className="flex items-center gap-3 px-4 py-3 border-b border-border hover:bg-secondary/30 transition-colors"
|
||||
>
|
||||
{communityImage ? (
|
||||
<img
|
||||
src={communityImage}
|
||||
alt={communityName}
|
||||
className="size-12 rounded-lg object-cover shrink-0"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-12 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
|
||||
<Users className="size-5 text-primary/50" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Users className="size-3 shrink-0" />
|
||||
<span>Community</span>
|
||||
{moderatorCount > 0 && (
|
||||
<span className="text-muted-foreground/60">· {moderatorCount} mod{moderatorCount !== 1 ? 's' : ''}</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm font-medium truncate mt-0.5">
|
||||
{communityName}
|
||||
</p>
|
||||
{communityDescription && (
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{communityDescription}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProfilePreview({ pubkey }: { pubkey: string }) {
|
||||
const author = useAuthor(pubkey);
|
||||
const metadata = author.data?.metadata;
|
||||
|
||||
@@ -71,7 +71,7 @@ import { ProfileHoverCard } from '@/components/ProfileHoverCard';
|
||||
import { useProfileUrl } from '@/hooks/useProfileUrl';
|
||||
import { ContentWarningGuard } from '@/components/ContentWarningGuard';
|
||||
import { MutedContentGuard } from '@/components/MutedContentGuard';
|
||||
import { ExternalContentPreview, ProfilePreview } from '@/components/ExternalContentHeader';
|
||||
import { ExternalContentPreview, ProfilePreview, CommunityPreview } from '@/components/ExternalContentHeader';
|
||||
import { getParentEventId, isReplyEvent } from '@/lib/nostrEvents';
|
||||
import { EmojiPackContent } from '@/components/EmojiPackContent';
|
||||
import { CommunityContent } from '@/components/CommunityContent';
|
||||
@@ -851,6 +851,21 @@ function PostDetailContent({ event }: { event: NostrEvent }) {
|
||||
return parts[1] || undefined;
|
||||
}, [event, isComment]);
|
||||
|
||||
// For kind 1111 comments on a community (kind 34550), extract the addr for the community preview
|
||||
const communityRootAddr = useMemo(() => {
|
||||
if (!isComment) return undefined;
|
||||
const kTag = event.tags.find(([n]) => n === 'K')?.[1];
|
||||
if (kTag !== '34550') return undefined;
|
||||
const aTag = event.tags.find(([n]) => n === 'A')?.[1];
|
||||
if (!aTag) return undefined;
|
||||
const parts = aTag.split(':');
|
||||
const kind = parseInt(parts[0], 10);
|
||||
const pubkey = parts[1];
|
||||
const identifier = parts.slice(2).join(':');
|
||||
if (!pubkey || isNaN(kind)) return undefined;
|
||||
return { kind, pubkey, identifier };
|
||||
}, [event, isComment]);
|
||||
|
||||
// Keep the focused post pinned to top while ancestor content loads above it.
|
||||
// A ResizeObserver on the ancestor container re-scrolls on every layout shift
|
||||
// (image loads, skeleton→content swaps) for the first few seconds.
|
||||
@@ -900,13 +915,16 @@ function PostDetailContent({ event }: { event: NostrEvent }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Content preview for kind 1111 comments: external content or profile */}
|
||||
{/* Content preview for kind 1111 comments: external content, profile, or community */}
|
||||
{externalIdentifier && (
|
||||
<ExternalContentPreview identifier={externalIdentifier} />
|
||||
)}
|
||||
{profileRootPubkey && (
|
||||
<ProfilePreview pubkey={profileRootPubkey} />
|
||||
)}
|
||||
{communityRootAddr && (
|
||||
<CommunityPreview addr={communityRootAddr} />
|
||||
)}
|
||||
|
||||
{/* Ancestor thread chain if this is a reply */}
|
||||
{parentEventId && (
|
||||
|
||||
Reference in New Issue
Block a user