Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 113 additions & 9 deletions apps/dashboard/src/components/details/detail-activity.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
import { useState } from "react";
import {
MarkdownEditor,
type MentionCandidate,
} from "@diffkit/ui/components/markdown-editor";
import { toast } from "@diffkit/ui/components/sonner";
import { Spinner } from "@diffkit/ui/components/spinner";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useMemo, useState } from "react";
import { createComment } from "#/lib/github.functions";
import {
type GitHubQueryScope,
githubQueryKeys,
githubRepoCollaboratorsQueryOptions,
githubViewerQueryOptions,
} from "#/lib/github.query";
import type { GitHubActor } from "#/lib/github.types";
import { checkPermissionWarning } from "#/lib/warning-store";

export function DetailActivityHeader({
title,
Expand All @@ -19,24 +35,112 @@ export function DetailActivityHeader({
);
}

export function DetailCommentBox() {
export function DetailCommentBox({
owner,
repo,
issueNumber,
scope,
involvedUsers,
}: {
owner: string;
repo: string;
issueNumber: number;
scope: GitHubQueryScope;
involvedUsers?: GitHubActor[];
}) {
const [value, setValue] = useState("");
const [isSending, setIsSending] = useState(false);
const [mentionActivated, setMentionActivated] = useState(false);
const queryClient = useQueryClient();

const viewerQuery = useQuery(githubViewerQueryOptions(scope));
const viewerLogin = viewerQuery.data?.login;

const collaboratorsQuery = useQuery({
...githubRepoCollaboratorsQueryOptions(scope, { owner, repo }),
enabled: mentionActivated,
});

const mentionCandidates: MentionCandidate[] = useMemo(() => {
const seen = new Set<string>();
const candidates: MentionCandidate[] = [];

// Exclude the current user
if (viewerLogin) seen.add(viewerLogin);

// Involved users first (commenters, reviewers, author)
if (involvedUsers) {
for (const user of involvedUsers) {
if (seen.has(user.login)) continue;
seen.add(user.login);
candidates.push({
id: user.login,
label: user.login,
avatarUrl: user.avatarUrl,
secondary: user.type === "Bot" ? "Bot" : undefined,
});
}
}

// Remaining collaborators
for (const c of collaboratorsQuery.data ?? []) {
if (seen.has(c.login)) continue;
seen.add(c.login);
candidates.push({
id: c.login,
label: c.login,
avatarUrl: c.avatarUrl,
secondary: c.type === "Bot" ? "Bot" : undefined,
});
}

return candidates;
}, [viewerLogin, involvedUsers, collaboratorsQuery.data]);

const handleSend = async () => {
if (!value.trim()) return;
setIsSending(true);
try {
const result = await createComment({
data: { owner, repo, issueNumber, body: value.trim() },
});
if (result.ok) {
setValue("");
void queryClient.invalidateQueries({
queryKey: githubQueryKeys.all,
});
} else {
toast.error(result.error);
checkPermissionWarning(result, `${owner}/${repo}`);
}
} catch {
toast.error("Failed to send comment");
} finally {
setIsSending(false);
}
};

return (
<div className="flex flex-col gap-2 rounded-lg border bg-surface-0 p-3">
<textarea
<div className="flex flex-col gap-2">
<MarkdownEditor
value={value}
onChange={(event) => setValue(event.target.value)}
onChange={setValue}
placeholder="Leave a comment..."
rows={3}
className="w-full resize-none bg-transparent text-sm outline-none placeholder:text-muted-foreground"
compact
mentions={{
candidates: mentionCandidates,
onActivate: () => setMentionActivated(true),
isLoading: collaboratorsQuery.isLoading && mentionActivated,
}}
/>
<div className="flex justify-end">
<button
type="button"
disabled={!value.trim()}
className="rounded-lg bg-foreground px-3 py-1.5 text-xs font-medium text-background transition-opacity disabled:opacity-40"
disabled={!value.trim() || isSending}
onClick={handleSend}
className="flex items-center gap-1.5 rounded-lg bg-foreground px-3 py-1.5 text-xs font-medium text-background transition-opacity disabled:opacity-40"
>
{isSending && <Spinner size={12} />}
Send
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import {
import { LabelPill } from "#/components/details/label-pill";
import { formatRelativeTime } from "#/lib/format-relative-time";
import { getCommentPage, getTimelineEventPage } from "#/lib/github.functions";
import type { GitHubQueryScope } from "#/lib/github.query";
import type {
CommentPagination,
EventPagination,
GitHubActor,
IssueComment,
IssuePageData,
TimelineEvent,
Expand Down Expand Up @@ -234,6 +236,8 @@ export function IssueDetailActivitySection({
owner,
repo,
issueNumber,
scope,
issueAuthor,
}: {
comments?: IssueComment[];
events?: TimelineEvent[];
Expand All @@ -244,6 +248,8 @@ export function IssueDetailActivitySection({
owner: string;
repo: string;
issueNumber: number;
scope: GitHubQueryScope;
issueAuthor: GitHubActor | null;
}) {
const allItems: IssueTimelineItem[] = [
...(comments ?? []).map((comment) => ({
Expand Down Expand Up @@ -423,12 +429,44 @@ export function IssueDetailActivitySection({
)}

<div className="mt-6">
<DetailCommentBox />
<DetailCommentBox
owner={owner}
repo={repo}
issueNumber={issueNumber}
scope={scope}
involvedUsers={getInvolvedUsers(issueAuthor, comments)}
/>
</div>
</div>
);
}

function getInvolvedUsers(
issueAuthor: GitHubActor | null,
comments?: IssueComment[],
): GitHubActor[] {
const seen = new Set<string>();
const users: GitHubActor[] = [];

const add = (actor: GitHubActor | null | undefined) => {
if (!actor || seen.has(actor.login)) return;
seen.add(actor.login);
users.push(actor);
};

// Issue author first
add(issueAuthor);

// Commenters (most recent first)
if (comments) {
for (let i = comments.length - 1; i >= 0; i--) {
add(comments[i].author);
}
}

return users;
}

// ── Load more divider ───────────────────────────────────────────────

function LoadMoreDivider({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export function IssueDetailPage() {
owner={owner}
repo={repo}
issueNumber={issueNumber}
scope={scope}
issueAuthor={issue.author}
/>
</>
}
Expand Down
Loading
Loading