/** * TopicView - Displays a full topic post with content and metadata. * Includes reactions, moderation controls, report button, edit button, and self-labels. * Used on the topic detail page. * @see specs/prd-web.md Section 4 (Topic Components) */ import Link from 'next/link' import Image from 'next/image' import { ChatCircle, Clock, Tag, PencilSimple, Link as LinkIcon, } from '@phosphor-icons/react/dist/ssr' import type { Topic } from '@/lib/api/types' import { cn } from '@/lib/utils' import { formatRelativeTime, formatCompactNumber, isEdited } from '@/lib/format' import { MarkdownContent } from './markdown-content' import { LikeButton } from './like-button' import { ReactionBar } from './reaction-bar' import { ModerationControls, type ModerationAction, type ModerationActionOptions, } from './moderation-controls' import { ReportDialog, type ReportSubmission } from './report-dialog' import { SelfLabelIndicator } from './self-label-indicator' interface ReactionData { type: string count: number reacted: boolean } interface TopicViewProps { topic: Topic reactions?: ReactionData[] onReactionToggle?: (type: string) => void isModerator?: boolean isAdmin?: boolean isLocked?: boolean isPinned?: boolean onModerationAction?: (action: ModerationAction, options?: ModerationActionOptions) => void canEdit?: boolean onEdit?: () => void onReply?: () => void canReport?: boolean onReport?: (report: ReportSubmission) => void isOwnContent?: boolean selfLabels?: string[] className?: string } export function TopicView({ topic, reactions, onReactionToggle, isModerator, isAdmin, isLocked, isPinned, onModerationAction, canEdit, onEdit, onReply, canReport, onReport, isOwnContent, selfLabels, className, }: TopicViewProps) { const headingId = `topic-heading-${topic.rkey}` const isDeleted = topic.isAuthorDeleted || topic.isModDeleted if (isDeleted) { const tombstoneText = topic.isModDeleted ? 'This topic was removed by a moderator.' : 'This topic was removed by the author.' return (

[deleted]

{tombstoneText}

) } return (
{/* Header */}

{topic.title}

{/* Author + timestamp */}
{topic.author?.avatarUrl ? ( ) : ( )} {topic.author?.displayName ?? topic.author?.handle ?? topic.authorDid} {isEdited(topic.publishedAt, topic.indexedAt) && ( (edited) )}
{/* Category + Tags */}
{topic.category} {topic.tags?.map((tag) => (
{/* Moderation controls */} {isModerator && onModerationAction && (
)}
{/* Content */}
{selfLabels && selfLabels.length > 0 ? ( ) : ( )}
{/* Footer: read signals left, actions right */}
{reactions && onReactionToggle && ( )} {canEdit && onEdit && ( )} {onReply ? ( ) : ( )} {canReport && onReport && }
) }