| 402 | 402 | | ); |
| 403 | 403 | | } |
| 404 | 404 | | |
| 405 | | − | /** Something that happened, as one line on the conversation's rail. */ |
| 406 | | − | function TimelineEvent({ comment, base, filesUrl }: { comment: Comment; base?: string; filesUrl?: string }) { |
| 405 | + | /** |
| 406 | + | * One line on the conversation's rail: a mark on the rail (a dot unless |
| 407 | + | * `mark` says otherwise), who, what and when. |
| 408 | + | */ |
| 409 | + | function TimelineRow({ comment, mark, children }: { comment: Comment; mark?: ReactNode; children: ReactNode }) { |
| 407 | 410 | | return ( |
| 408 | 411 | | <div className="flex items-center gap-3 text-sm text-muted"> |
| 409 | 412 | | <span className="hidden w-8 shrink-0 justify-center sm:flex"> |
| 410 | | − | <span className="size-2 rounded-full border border-line-strong bg-bg" /> |
| 413 | + | {mark ?? <span className="size-2 rounded-full border border-line-strong bg-bg" />} |
| 411 | 414 | | </span> |
| 412 | 415 | | <span className="flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-0.5"> |
| 413 | | − | <Avatar name={comment.author.username} size={16} /> |
| 414 | | − | <PersonLink name={comment.author.username} className="font-medium text-fg hover:underline" /> |
| 415 | | − | <span> |
| 416 | | − | <WithReferences text={comment.body} base={base} filesUrl={filesUrl} /> |
| 417 | | − | </span> |
| 416 | + | {comment.agent ? ( |
| 417 | + | <> |
| 418 | + | <Avatar name={comment.agent.displayName} agent size={16} /> |
| 419 | + | <AgentName agent={comment.agent} /> |
| 420 | + | </> |
| 421 | + | ) : ( |
| 422 | + | <> |
| 423 | + | <Avatar name={comment.author.username} size={16} /> |
| 424 | + | <PersonLink name={comment.author.username} className="font-medium text-fg hover:underline" /> |
| 425 | + | </> |
| 426 | + | )} |
| 427 | + | {children} |
| 428 | + | {comment.actingFor && ( |
| 429 | + | <span className="text-xs text-faint"> |
| 430 | + | on behalf of{" "} |
| 431 | + | <PersonLink name={comment.actingFor.username} className="hover:text-fg hover:underline"> |
| 432 | + | @{comment.actingFor.username} |
| 433 | + | </PersonLink> |
| 434 | + | </span> |
| 435 | + | )} |
| 418 | 436 | | <span className="text-faint"> |
| 419 | 437 | | · <TimeAgo at={comment.createdAt} /> |
| 420 | 438 | | </span> |
| ⋯ |
| 423 | 441 | | ); |
| 424 | 442 | | } |
| 425 | 443 | | |
| 444 | + | /** Something that happened, as one line on the conversation's rail. */ |
| 445 | + | function TimelineEvent({ comment, base, filesUrl }: { comment: Comment; base?: string; filesUrl?: string }) { |
| 446 | + | return ( |
| 447 | + | <TimelineRow comment={comment}> |
| 448 | + | <span> |
| 449 | + | <WithReferences text={comment.body} base={base} filesUrl={filesUrl} /> |
| 450 | + | </span> |
| 451 | + | </TimelineRow> |
| 452 | + | ); |
| 453 | + | } |
| 454 | + | |
| 455 | + | /** A review's verdict, approved or changes requested, in its own colour. */ |
| 456 | + | function Verdict({ verdict }: { verdict: keyof typeof VERDICTS }) { |
| 457 | + | const { label, style } = VERDICTS[verdict]; |
| 458 | + | return ( |
| 459 | + | <span className={`flex items-center gap-1 font-medium ${style}`}> |
| 460 | + | {verdict === "approve" ? <CircleCheck size={14} /> : <CircleSlash size={14} />} |
| 461 | + | {label} |
| 462 | + | </span> |
| 463 | + | ); |
| 464 | + | } |
| 465 | + | |
| 466 | + | /** |
| 467 | + | * A review that gave a verdict and said nothing else: one line on the rail, |
| 468 | + | * not a comment with an empty body. There is nothing to edit, and a |
| 469 | + | * verdict is not deleted, so it has no actions. |
| 470 | + | */ |
| 471 | + | function TimelineVerdict({ comment, verdict }: { comment: Comment; verdict: keyof typeof VERDICTS }) { |
| 472 | + | return ( |
| 473 | + | <TimelineRow |
| 474 | + | comment={comment} |
| 475 | + | mark={ |
| 476 | + | verdict === "approve" ? ( |
| 477 | + | <CircleCheck size={16} className="text-success" /> |
| 478 | + | ) : ( |
| 479 | + | <CircleSlash size={16} className="text-danger" /> |
| 480 | + | ) |
| 481 | + | } |
| 482 | + | > |
| 483 | + | <span className={VERDICTS[verdict].style}>{VERDICTS[verdict].label}</span> |
| 484 | + | {comment.advisory && <AdvisoryChip />} |
| 485 | + | </TimelineRow> |
| 486 | + | ); |
| 487 | + | } |
| 488 | + | |
| 426 | 489 | | /** Edit and Delete, under a comment the viewer may change. */ |
| 427 | 490 | | function CommentActions({ |
| 428 | 491 | | comment, |
| ⋯ |
| 521 | 584 | | if (comment.kind === "event") { |
| 522 | 585 | | return <TimelineEvent key={comment.id} comment={comment} base={base} filesUrl={review?.changesUrl} />; |
| 523 | 586 | | } |
| 524 | | − | const verdict = comment.verdict && VERDICTS[comment.verdict]; |
| 587 | + | const verdict = comment.verdict; |
| 588 | + | // A verdict with nothing said and no line it is about is one line, not a card. |
| 589 | + | if (verdict && !comment.body.trim() && !comment.path) { |
| 590 | + | return <TimelineVerdict key={comment.id} comment={comment} verdict={verdict} />; |
| 591 | + | } |
| 525 | 592 | | const can = mayChangeComment(comment, viewerId, canModerate); |
| 526 | 593 | | return ( |
| 527 | 594 | | <TimelineItem |
| ⋯ |
| 537 | 604 | | action={ |
| 538 | 605 | | <> |
| 539 | 606 | | {verdict ? ( |
| 540 | | − | <span className={`flex items-center gap-1 font-medium ${verdict.style}`}> |
| 541 | | − | {comment.verdict === "approve" ? ( |
| 542 | | − | <CircleCheck size={14} /> |
| 543 | | − | ) : ( |
| 544 | | − | <CircleSlash size={14} /> |
| 545 | | − | )} |
| 546 | | − | {verdict.label} |
| 547 | | − | </span> |
| 607 | + | <Verdict verdict={verdict} /> |
| 548 | 608 | | ) : ( |
| 549 | 609 | | <span>{comment.advisory ? "reviewed" : "commented"}</span> |
| 550 | 610 | | )} |