import { CircleCheck, CircleDot, CircleX, MessageSquare } from "lucide-react"; import * as React from "react"; import { toast } from "sonner"; import { ForumComposer } from "@/features/forum/ui/ForumComposer"; import { type Project, type ProjectIssue, useCreateProjectIssueCommentMutation, useProjectIssuesQuery, } from "@/features/projects/hooks"; import { resolveUserLabel, type UserProfileLookup, } from "@/features/profile/lib/identity"; import { relativeTime } from "@/features/projects/lib/projectsViewHelpers"; import type { ChannelMember } from "@/shared/api/types"; import { normalizePubkey } from "@/shared/lib/pubkey"; import { ProjectFeedRow, ProjectFeedRowCluster, ProjectFeedRowMonoCell, } from "./ProjectFeedRow"; import { OverviewRailSection } from "./ProjectOverviewPanel"; import { ProfileIdentityButton } from "./ProjectProfileIdentity"; import { ProjectRichContent } from "./ProjectRichContent"; export function issueStatusClassName(status: ProjectIssue["status"]) { if (status === "Done") return "text-purple-400"; if (status === "Closed") return "text-destructive"; return "text-green-500"; } function issueStatusVisual(status: ProjectIssue["status"]) { if (status === "Done") { return { className: "text-purple-400", icon: CircleCheck }; } if (status === "Closed") { return { className: "text-destructive", icon: CircleX }; } return { className: "text-green-500", icon: CircleDot }; } function issueMembers( project: Project, issue: ProjectIssue, profiles?: UserProfileLookup, ): ChannelMember[] { return [ ...new Set([ project.owner, issue.author, ...project.contributors, ...issue.recipients, ]), ].map((pubkey) => { const profile = profiles?.[normalizePubkey(pubkey)]; return { pubkey, role: "member" as const, isAgent: profile?.isAgent === true, joinedAt: new Date(0).toISOString(), displayName: profile?.displayName?.trim() || profile?.nip05Handle?.trim() || null, }; }); } function AuthorIdentity({ profiles, pubkey, role, }: { profiles?: UserProfileLookup; pubkey: string; role?: React.ReactNode; }) { const profile = profiles?.[normalizePubkey(pubkey)]; return ( ); } function IssueRow({ issue, onOpen, profiles, }: { issue: ProjectIssue; onOpen: () => void; profiles?: UserProfileLookup; }) { const authorProfile = profiles?.[normalizePubkey(issue.author)]; const authorLabel = resolveUserLabel({ profiles, pubkey: issue.author }); const status = issueStatusVisual(issue.status); return ( {authorLabel} created this issue {relativeTime(issue.createdAt)} · {issue.status} {issue.labels.map((label) => ( {label} ))} } onOpen={onOpen} statusIcon={ } testId="project-issue-row" title={issue.title} trailing={ <> {issue.comments.length > 0 ? ( {issue.comments.length} ) : null} } /> ); } function IssueDetail({ issue, profiles, project, }: { issue: ProjectIssue; profiles?: UserProfileLookup; project: Project; }) { const commentMutation = useCreateProjectIssueCommentMutation(project); const authorLabel = resolveUserLabel({ profiles, pubkey: issue.author }); const members = React.useMemo( () => issueMembers(project, issue, profiles), [issue, profiles, project], ); const handleCommentSubmit = React.useCallback( async ( content: string, mentionPubkeys: string[], mediaTags?: string[][], ) => { try { await commentMutation.mutateAsync({ content, issue, mediaTags, mentionPubkeys, }); toast.success("Comment posted."); } catch (error) { toast.error( error instanceof Error ? error.message : "Failed to post comment.", ); throw error; } }, [commentMutation, issue], ); return (

Issue from {authorLabel}

{issue.title}{" "} #{issue.id.slice(0, 8)}

{issue.content ? ( ) : null}

Add Your Comment

{issue.comments.length > 0 ? (
{issue.comments.map((item) => (
))}
) : (

No comments yet.

)}
); } /** Right-hand meta column for the issue detail view: status, author, labels, * and dates — keeps the conversation column focused. */ function IssueMetaRail({ issue, profiles, }: { issue: ProjectIssue; profiles?: UserProfileLookup; }) { const authorProfile = profiles?.[normalizePubkey(issue.author)]; const authorLabel = resolveUserLabel({ profiles, pubkey: issue.author }); const status = issueStatusVisual(issue.status); return ( ); } export function ProjectIssuesPanel({ onSelectedIssueIdChange, profiles, project, selectedIssueId, }: { onSelectedIssueIdChange: (id: string | null) => void; profiles?: UserProfileLookup; project: Project; selectedIssueId: string | null; }) { const issuesQuery = useProjectIssuesQuery(project); const issues = issuesQuery.data ?? []; const selectedIssue = issues.find((issue) => issue.id === selectedIssueId) ?? null; if (issuesQuery.isLoading) { return

Loading issues…

; } if (issues.length === 0) { return (

{issuesQuery.error ? "Could not load issues for this repository." : "No issues yet."}

); } if (selectedIssue) { return ( ); } return (
{issues.map((issue) => ( onSelectedIssueIdChange(issue.id)} profiles={profiles} /> ))}
); }