mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-15 19:30:38 +08:00
Implement code comments for single line selections
This commit is contained in:
@@ -22,6 +22,11 @@ body {
|
||||
background-color: var(--color-background-default);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--color-fill-yellow);
|
||||
color: var(--color-foreground-black);
|
||||
}
|
||||
|
||||
.global-oid {
|
||||
color: var(--color-foreground-emphasized);
|
||||
font-size: var(--font-size-small);
|
||||
|
||||
+33
-33
@@ -1,67 +1,67 @@
|
||||
.syntax.operator,
|
||||
.syntax.keyword\.repeat,
|
||||
.syntax.keyword {
|
||||
.global-syntax.operator,
|
||||
.global-syntax.keyword\.repeat,
|
||||
.global-syntax.keyword {
|
||||
color: var(--color-prettylights-syntax-keyword);
|
||||
}
|
||||
.syntax.type {
|
||||
.global-syntax.type {
|
||||
color: var(--color-prettylights-syntax-entity);
|
||||
}
|
||||
.syntax.property,
|
||||
.syntax.variable\.parameter {
|
||||
.global-syntax.property,
|
||||
.global-syntax.variable\.parameter {
|
||||
color: var(--color-prettylights-syntax-variable);
|
||||
}
|
||||
.syntax.punctuation\.delimiter {
|
||||
.global-syntax.punctuation\.delimiter {
|
||||
}
|
||||
.syntax.punctuation\.bracket {
|
||||
.global-syntax.punctuation\.bracket {
|
||||
}
|
||||
.syntax.attribute {
|
||||
.global-syntax.attribute {
|
||||
}
|
||||
.syntax.number,
|
||||
.syntax.constant,
|
||||
.syntax.type\.builtin,
|
||||
.syntax.constant\.builtin,
|
||||
.syntax.variable\.builtin,
|
||||
.syntax.function {
|
||||
.global-syntax.number,
|
||||
.global-syntax.constant,
|
||||
.global-syntax.type\.builtin,
|
||||
.global-syntax.constant\.builtin,
|
||||
.global-syntax.variable\.builtin,
|
||||
.global-syntax.function {
|
||||
color: var(--color-prettylights-syntax-constant);
|
||||
}
|
||||
.syntax.comment,
|
||||
.syntax.comment\.documentation {
|
||||
.global-syntax.comment,
|
||||
.global-syntax.comment\.documentation {
|
||||
color: var(--color-prettylights-syntax-comment);
|
||||
}
|
||||
.syntax.string {
|
||||
.global-syntax.string {
|
||||
color: var(--color-prettylights-syntax-string);
|
||||
}
|
||||
.syntax.string.special {
|
||||
.global-syntax.string.special {
|
||||
}
|
||||
.syntax.function\.method {
|
||||
.global-syntax.function\.method {
|
||||
color: var(--color-prettylights-syntax-entity);
|
||||
}
|
||||
.syntax.type.builtin {
|
||||
.global-syntax.type.builtin {
|
||||
}
|
||||
.syntax.punctuation.bracket {
|
||||
.global-syntax.punctuation.bracket {
|
||||
}
|
||||
.syntax.punctuation.delimiter {
|
||||
.global-syntax.punctuation.delimiter {
|
||||
}
|
||||
.syntax.punctuation.special {
|
||||
.global-syntax.punctuation.special {
|
||||
}
|
||||
.syntax.text.literal {
|
||||
.global-syntax.text.literal {
|
||||
}
|
||||
.syntax.text.title {
|
||||
.global-syntax.text.title {
|
||||
}
|
||||
.syntax.variable {
|
||||
.global-syntax.variable {
|
||||
color: var(--color-prettylights-syntax-storage-modifier-import);
|
||||
}
|
||||
.syntax.attribute {
|
||||
.global-syntax.attribute {
|
||||
}
|
||||
.syntax.label {
|
||||
.global-syntax.label {
|
||||
}
|
||||
.syntax.type {
|
||||
.global-syntax.type {
|
||||
}
|
||||
.syntax.variable.parameter {
|
||||
.global-syntax.variable.parameter {
|
||||
}
|
||||
.syntax.constructor {
|
||||
.global-syntax.constructor {
|
||||
color: var(--color-prettylights-syntax-entity);
|
||||
}
|
||||
.syntax.tag\.delimiter {
|
||||
.global-syntax.tag\.delimiter {
|
||||
color: var(--color-prettylights-syntax-keyword);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
children: Snippet;
|
||||
variant: "primary" | "secondary" | "ghost" | "float" | "danger" | "success";
|
||||
hoverable?: boolean;
|
||||
onclick?: () => void;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
stylePosition?: string;
|
||||
stylePadding?: string;
|
||||
styleHeight?: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Commit } from "@bindings/repo/Commit";
|
||||
import type { Diff } from "@bindings/diff/Diff";
|
||||
import type { CodeComments } from "./Diff.svelte";
|
||||
import type { Revision } from "@bindings/cob/patch/Revision";
|
||||
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
@@ -17,10 +18,17 @@
|
||||
revision: Revision;
|
||||
rid: string;
|
||||
showCommits?: boolean;
|
||||
codeComments?: CodeComments;
|
||||
}
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
let { patchId, revision, rid, showCommits = true }: Props = $props();
|
||||
let {
|
||||
patchId,
|
||||
revision,
|
||||
rid,
|
||||
showCommits = true,
|
||||
codeComments,
|
||||
}: Props = $props();
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
let hideChanges = $state(false);
|
||||
@@ -39,7 +47,7 @@
|
||||
options: {
|
||||
base,
|
||||
head,
|
||||
unified: 5,
|
||||
unified: 3,
|
||||
highlight: true,
|
||||
},
|
||||
});
|
||||
@@ -144,6 +152,10 @@
|
||||
{#await loadHighlightedDiff(rid, revision.base, revision.head)}
|
||||
<span class="txt-small">Loading…</span>
|
||||
{:then diff}
|
||||
<Changeset {diff} repoId={rid} expanded={filesExpanded} />
|
||||
<Changeset
|
||||
expanded={filesExpanded}
|
||||
head={revision.head}
|
||||
{diff}
|
||||
{codeComments} />
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { Diff } from "@bindings/diff/Diff";
|
||||
import type { CodeComments } from "./Diff.svelte";
|
||||
|
||||
import FileDiff from "./Changeset/FileDiff.svelte";
|
||||
import FileDiffComponent from "./FileDiff.svelte";
|
||||
|
||||
interface Props {
|
||||
codeComments?: CodeComments;
|
||||
diff: Diff;
|
||||
repoId: string;
|
||||
expanded?: boolean;
|
||||
head: string;
|
||||
}
|
||||
|
||||
const { diff, expanded = true }: Props = $props();
|
||||
const { codeComments, diff, expanded = true, head }: Props = $props();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -26,12 +28,7 @@
|
||||
<div class="diff-list">
|
||||
{#each diff.files as file}
|
||||
<div class="diff">
|
||||
<FileDiff
|
||||
{expanded}
|
||||
filePath={"path" in file ? file.path : file.newPath}
|
||||
oldFilePath={"oldPath" in file ? file.oldPath : undefined}
|
||||
fileDiff={file.diff}
|
||||
headerBadgeCaption={file.status} />
|
||||
<FileDiffComponent {codeComments} {expanded} {file} {head} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { DiffContent } from "@bindings/diff/DiffContent";
|
||||
import type { FileDiff } from "@bindings/diff/FileDiff";
|
||||
import type { Modification } from "@bindings/diff/Modification";
|
||||
|
||||
import escape from "lodash/escape";
|
||||
|
||||
import File from "@app/components/File.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
|
||||
interface Props {
|
||||
filePath: string;
|
||||
oldFilePath?: string | undefined;
|
||||
fileDiff: DiffContent;
|
||||
headerBadgeCaption: FileDiff["status"];
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
filePath,
|
||||
oldFilePath = undefined,
|
||||
fileDiff,
|
||||
headerBadgeCaption,
|
||||
expanded,
|
||||
}: Props = $props();
|
||||
|
||||
function lineNumberR(line: Modification): string | number {
|
||||
switch (line.type) {
|
||||
case "addition": {
|
||||
return line.lineNo;
|
||||
}
|
||||
case "context": {
|
||||
return line.lineNoNew;
|
||||
}
|
||||
case "deletion": {
|
||||
return " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lineNumberL(line: Modification): string | number {
|
||||
switch (line.type) {
|
||||
case "addition": {
|
||||
return " ";
|
||||
}
|
||||
case "context": {
|
||||
return line.lineNoOld;
|
||||
}
|
||||
case "deletion": {
|
||||
return line.lineNo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lineSign(line: Modification): string {
|
||||
switch (line.type) {
|
||||
case "addition": {
|
||||
return "+";
|
||||
}
|
||||
case "context": {
|
||||
return " ";
|
||||
}
|
||||
case "deletion": {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
font-size: var(--font-size-small);
|
||||
overflow-x: auto;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
.browse {
|
||||
margin-left: auto;
|
||||
}
|
||||
.expand-button {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.diff {
|
||||
font-family: var(--font-family-monospace);
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
margin: 0.5rem 0;
|
||||
-webkit-touch-callout: initial;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
.diff-line {
|
||||
vertical-align: top;
|
||||
}
|
||||
.diff-line.type-addition > * {
|
||||
background-color: var(--color-fill-diff-green-light);
|
||||
}
|
||||
.diff-line.type-deletion > * {
|
||||
background-color: var(--color-fill-diff-red-light);
|
||||
}
|
||||
|
||||
.diff-line.selected > * {
|
||||
background-color: var(--color-fill-float-hover);
|
||||
}
|
||||
.diff-line.selected.type-addition > * {
|
||||
background-color: var(--color-fill-diff-green);
|
||||
}
|
||||
.diff-line.selected.type-deletion > * {
|
||||
background-color: var(--color-fill-diff-red);
|
||||
}
|
||||
|
||||
.type-addition > .diff-line-number,
|
||||
.type-addition > .diff-line-type {
|
||||
color: var(--color-foreground-success);
|
||||
}
|
||||
.type-deletion > .diff-line-number,
|
||||
.type-deletion > .diff-line-type {
|
||||
color: var(--color-foreground-red);
|
||||
}
|
||||
|
||||
.diff-line.selected .selection-indicator-left {
|
||||
background-color: var(--color-fill-secondary);
|
||||
}
|
||||
.type-addition.diff-line.selected .selection-indicator-left {
|
||||
background-color: var(--color-fill-secondary);
|
||||
}
|
||||
.type-deletion.diff-line.selected .selection-indicator-left {
|
||||
background-color: var(--color-fill-secondary);
|
||||
}
|
||||
|
||||
.diff-line.selected .selection-indicator-right {
|
||||
background-color: var(--color-fill-secondary);
|
||||
}
|
||||
.type-addition.diff-line.selected .selection-indicator-right {
|
||||
background-color: var(--color-fill-secondary);
|
||||
}
|
||||
.type-deletion.diff-line.selected .selection-indicator-right {
|
||||
background-color: var(--color-fill-secondary);
|
||||
}
|
||||
|
||||
.selection-start {
|
||||
box-shadow: 0 -1px 0 0 var(--color-fill-secondary);
|
||||
z-index: 1;
|
||||
}
|
||||
.selection-end {
|
||||
box-shadow: 0 1px 0 0 var(--color-fill-secondary);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.selection-start.selection-end {
|
||||
box-shadow: 0 0 0 1px var(--color-fill-secondary);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.diff-line-number {
|
||||
font-family: var(--font-family-monospace);
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
line-height: 1.5rem;
|
||||
min-width: 3rem;
|
||||
color: var(--color-foreground-disabled);
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.diff-line-number.left {
|
||||
position: relative;
|
||||
padding: 0 0.5rem 0 0.75rem;
|
||||
}
|
||||
.selection-indicator-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
}
|
||||
.selection-indicator-right {
|
||||
display: none; /* FIXME: fix the selection indicator */
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
}
|
||||
.diff-line-number.right {
|
||||
padding: 0 0.75rem 0 0.5rem;
|
||||
}
|
||||
.diff-line-content {
|
||||
color: unset !important;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
width: 100%;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
.diff-line-type {
|
||||
text-align: center;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.diff-expand-header {
|
||||
padding-left: 0.5rem;
|
||||
color: var(--color-foreground-dim);
|
||||
}
|
||||
.added {
|
||||
color: var(--color-foreground-success);
|
||||
background-color: var(--color-fill-diff-green-light);
|
||||
}
|
||||
.deleted {
|
||||
color: var(--color-foreground-red);
|
||||
background-color: var(--color-fill-diff-red-light);
|
||||
}
|
||||
.moved,
|
||||
.copied {
|
||||
color: var(--color-foreground-dim);
|
||||
background: var(--color-fill-ghost);
|
||||
}
|
||||
</style>
|
||||
|
||||
{#snippet styledPath(fullPath: string)}
|
||||
<!-- prettier-ignore -->
|
||||
<span class="txt-small" style:white-space="nowrap" style:-webkit-touch-callout="initial" style:-webkit-user-select="text" style:user-select="text"><span style:color="var(--color-fill-gray)" style:font-weight="var(--font-weight-regular)">{fullPath.match(/^.*\/|/)?.values().next().value}</span><span style:font-weight="var(--font-weight-semibold)">{fullPath.split("/").slice(-1)}</span></span>
|
||||
{/snippet}
|
||||
|
||||
{#snippet emptyPlaceholder()}
|
||||
<div class="global-flex" style:margin="1rem 0" style:justify-content="center">
|
||||
<Icon name="none" />Empty file
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<File {expanded}>
|
||||
{#snippet leftHeader()}
|
||||
{#if (headerBadgeCaption === "moved" || headerBadgeCaption === "copied") && oldFilePath}
|
||||
<span style="display: flex; align-items: center; flex-wrap: wrap;">
|
||||
{@render styledPath(filePath)}
|
||||
<span style:padding="0 0.5rem">→</span>
|
||||
{@render styledPath(filePath)}
|
||||
</span>
|
||||
{:else}
|
||||
{@render styledPath(filePath)}
|
||||
{/if}
|
||||
|
||||
{#if headerBadgeCaption === "added"}
|
||||
<span class="global-counter added">added</span>
|
||||
{:else if headerBadgeCaption === "deleted"}
|
||||
<span class="global-counter deleted">deleted</span>
|
||||
{:else if headerBadgeCaption === "moved"}
|
||||
<span class="global-counter moved">moved</span>
|
||||
{:else if headerBadgeCaption === "copied"}
|
||||
<span class="global-counter copied">copied</span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet children()}
|
||||
<div class="container">
|
||||
{#if fileDiff.type === "plain"}
|
||||
{#if fileDiff.hunks.length > 0}
|
||||
<table class="diff" data-file-diff-select>
|
||||
{#each fileDiff.hunks as hunk, hunkIdx}
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
<tr class="diff-line hunk-header">
|
||||
<td colspan={2} style:position="relative">
|
||||
<div class="selection-indicator-left"></div>
|
||||
</td>
|
||||
<td
|
||||
colspan={6}
|
||||
class="diff-expand-header"
|
||||
style:position="relative">
|
||||
{hunk.header}
|
||||
<div class="selection-indicator-right"></div>
|
||||
</td>
|
||||
</tr>
|
||||
{#each hunk.lines as line, lineIdx}
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
<tr
|
||||
style="position: relative;"
|
||||
class={`diff-line type-${line.type}`}>
|
||||
<td
|
||||
id={[filePath, "H" + hunkIdx, "L" + lineIdx].join("-")}
|
||||
class="diff-line-number left">
|
||||
<div class="selection-indicator-left"></div>
|
||||
{lineNumberL(line)}
|
||||
</td>
|
||||
<td class="diff-line-number right">
|
||||
{lineNumberR(line)}
|
||||
</td>
|
||||
<td class="diff-line-type" data-line-type={line.type}>
|
||||
{lineSign(line)}
|
||||
</td>
|
||||
<td class="diff-line-content">
|
||||
{#if line.highlight?.items.length === 0 || line.line === ""}
|
||||
<br />
|
||||
{:else if line.highlight}
|
||||
{@html line.highlight.items
|
||||
.map(
|
||||
s =>
|
||||
`<span class="syntax ${s.style}">${escape(s.item)}</span>`,
|
||||
)
|
||||
.join("")}
|
||||
{:else}
|
||||
{line.line}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="selection-indicator-right"></td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/each}
|
||||
</table>
|
||||
{:else}
|
||||
{@render emptyPlaceholder()}
|
||||
{/if}
|
||||
{:else}
|
||||
{@render emptyPlaceholder()}
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</File>
|
||||
@@ -38,6 +38,7 @@
|
||||
padding: 0 0.75rem;
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-fill-gray);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -71,7 +72,10 @@
|
||||
variant="float"
|
||||
styleHeight="40px"
|
||||
styleWidth="100%"
|
||||
onclick={() => {
|
||||
onclick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
state = "expanded";
|
||||
if (onexpand !== undefined) {
|
||||
onexpand();
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
<script lang="ts" module>
|
||||
export interface CodeComments {
|
||||
threads: Thread<CodeLocation>[];
|
||||
config: Config;
|
||||
createComment: (
|
||||
body: string,
|
||||
embeds: Embed[],
|
||||
replyTo?: string,
|
||||
location?: CodeLocation,
|
||||
) => Promise<void>;
|
||||
editComment: (
|
||||
commentId: string,
|
||||
body: string,
|
||||
embeds: Embed[],
|
||||
) => Promise<void>;
|
||||
reactOnComment: (
|
||||
publicKey: string,
|
||||
commentId: string,
|
||||
authors: Author[],
|
||||
reaction: string,
|
||||
) => Promise<void>;
|
||||
repoDelegates: Author[];
|
||||
rid: string;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
type Side = "left" | "right";
|
||||
type SelectionAnchor = { side: Side; lineNumber: number };
|
||||
type SelectionRange = { start: SelectionAnchor; end?: SelectionAnchor };
|
||||
|
||||
interface Selection {
|
||||
file: string;
|
||||
start: SelectionAnchor;
|
||||
end: SelectionAnchor;
|
||||
lineIdx: number;
|
||||
hunkIdx: number;
|
||||
codeLocation: CodeLocation;
|
||||
}
|
||||
|
||||
import type { Author } from "@bindings/cob/Author";
|
||||
import type { CodeLocation } from "@bindings/cob/thread/CodeLocation";
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
import type { Embed } from "@bindings/cob/thread/Embed";
|
||||
import type { FileDiff } from "@bindings/diff/FileDiff";
|
||||
import type { Modification } from "@bindings/diff/Modification";
|
||||
import type { Thread } from "@bindings/cob/thread/Thread";
|
||||
|
||||
import * as roles from "@app/lib/roles";
|
||||
|
||||
import escape from "lodash/escape";
|
||||
import partial from "lodash/partial";
|
||||
|
||||
import CommentToggleInput from "./CommentToggleInput.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import ThreadComponent from "@app/components/Thread.svelte";
|
||||
|
||||
interface Props {
|
||||
codeComments?: CodeComments;
|
||||
file: FileDiff;
|
||||
head: string;
|
||||
}
|
||||
|
||||
const { file, head, codeComments }: Props = $props();
|
||||
|
||||
let selection: Selection | undefined = $state(undefined);
|
||||
|
||||
function lineNumber(line: Modification, side: Side): number | undefined {
|
||||
if (side === "left") {
|
||||
if (line.type === "context") {
|
||||
return line.lineNoOld;
|
||||
}
|
||||
if (line.type === "deletion") {
|
||||
return line.lineNo;
|
||||
}
|
||||
} else {
|
||||
if (line.type === "context") {
|
||||
return line.lineNoNew;
|
||||
}
|
||||
if (line.type === "addition") {
|
||||
return line.lineNo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findLineThread(line: Modification) {
|
||||
return codeComments?.threads.find(t => {
|
||||
if (line.type === "addition") {
|
||||
return t.root.location?.new?.range.end === line.lineNo + 1;
|
||||
} else if (line.type === "deletion") {
|
||||
return t.root.location?.old?.range.end === line.lineNo + 1;
|
||||
} else if (line.type === "context") {
|
||||
return (
|
||||
t.root.location?.new?.range.end === line.lineNoNew + 1 ||
|
||||
t.root.location?.old?.range.end === line.lineNoOld + 1
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function determineSelectedAnchor(
|
||||
side: Side,
|
||||
line: Modification,
|
||||
): SelectionAnchor {
|
||||
// When a user tries to select a side with no changes, use opposite side.
|
||||
if (side === "left" && line.type === "addition") {
|
||||
return { side: "right", lineNumber: line.lineNo };
|
||||
} else if (side === "right" && line.type === "deletion") {
|
||||
return { side: "left", lineNumber: line.lineNo };
|
||||
} else {
|
||||
return side === "left"
|
||||
? { side: "left", lineNumber: lineNumber(line, "left") as number }
|
||||
: { side: "right", lineNumber: lineNumber(line, "right") as number };
|
||||
}
|
||||
}
|
||||
|
||||
function filePath(file: FileDiff, side: Side): string {
|
||||
if (file.status === "moved" || file.status === "copied") {
|
||||
if (side === "left") {
|
||||
return file.oldPath;
|
||||
} else {
|
||||
return file.newPath;
|
||||
}
|
||||
} else {
|
||||
return file.path;
|
||||
}
|
||||
}
|
||||
|
||||
function selectLine(
|
||||
e: MouseEvent,
|
||||
file: FileDiff,
|
||||
side: Side,
|
||||
line: Modification,
|
||||
hunkIdx: number,
|
||||
lineIdx: number,
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
selection = {
|
||||
file: filePath(file, side),
|
||||
start: determineSelectedAnchor(side, line),
|
||||
end: determineSelectedAnchor(side, line),
|
||||
hunkIdx: hunkIdx,
|
||||
lineIdx: lineIdx,
|
||||
codeLocation: {
|
||||
commit: head,
|
||||
path: filePath(file, side),
|
||||
old:
|
||||
side === "left"
|
||||
? {
|
||||
type: "lines",
|
||||
range: {
|
||||
start: lineNumber(line, "left") as number,
|
||||
end: (lineNumber(line, "left") as number) + 1,
|
||||
},
|
||||
}
|
||||
: null,
|
||||
new:
|
||||
side === "right"
|
||||
? {
|
||||
type: "lines",
|
||||
range: {
|
||||
start: lineNumber(line, "right") as number,
|
||||
end: (lineNumber(line, "right") as number) + 1,
|
||||
},
|
||||
}
|
||||
: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isSelected(file: string, hunkIdx: number, lineIdx: number) {
|
||||
return (
|
||||
selection &&
|
||||
selection.file === file &&
|
||||
selection.hunkIdx === hunkIdx &&
|
||||
selection.lineIdx === lineIdx
|
||||
);
|
||||
}
|
||||
|
||||
function rangeAnchorsFromCodeLocation(
|
||||
location: CodeLocation | null,
|
||||
): SelectionRange | undefined {
|
||||
if (location?.old?.type === "lines") {
|
||||
return {
|
||||
start: { side: "left", lineNumber: location.old.range.start },
|
||||
};
|
||||
} else if (location?.new?.type === "lines") {
|
||||
return {
|
||||
start: { side: "right", lineNumber: location.new.range.start },
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
/* Make space for the box-shadow border, otherwise it gets cut off due to
|
||||
overflow: hide on the container. */
|
||||
padding: 8px 1px;
|
||||
font-size: var(--font-size-small);
|
||||
font-family: var(--font-family-monospace);
|
||||
}
|
||||
.line {
|
||||
display: flex;
|
||||
position: relative;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.hunk-header {
|
||||
color: var(--color-foreground-dim);
|
||||
}
|
||||
.hunk-header > .left,
|
||||
.hunk-header > .right {
|
||||
cursor: default;
|
||||
}
|
||||
.addition {
|
||||
background-color: var(--color-fill-diff-green-light);
|
||||
}
|
||||
.deletion {
|
||||
background-color: var(--color-fill-diff-red-light);
|
||||
}
|
||||
.addition > .left,
|
||||
.addition > .right,
|
||||
.addition > .sign {
|
||||
color: var(--color-foreground-success);
|
||||
}
|
||||
.deletion > .left,
|
||||
.deletion > .right,
|
||||
.deletion > .sign {
|
||||
color: var(--color-foreground-red);
|
||||
}
|
||||
.context > .left,
|
||||
.context > .right,
|
||||
.context > .sign {
|
||||
color: var(--color-foreground-disabled);
|
||||
}
|
||||
.marker {
|
||||
color: var(--color-foreground-contrast) !important;
|
||||
}
|
||||
.selected {
|
||||
box-shadow: 0 0 0 1px var(--color-fill-secondary);
|
||||
z-index: 1;
|
||||
}
|
||||
.left,
|
||||
.right {
|
||||
min-width: 3rem;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
cursor: cell;
|
||||
}
|
||||
.left:hover,
|
||||
.right:hover,
|
||||
.left:active,
|
||||
.right:active {
|
||||
color: var(--color-foreground-contrast);
|
||||
}
|
||||
.sign {
|
||||
min-width: 1.5rem;
|
||||
}
|
||||
.code {
|
||||
-webkit-touch-callout: initial;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
width: 100%;
|
||||
word-break: break-word;
|
||||
}
|
||||
.comment-icon {
|
||||
margin-left: auto;
|
||||
margin-right: 1rem;
|
||||
margin-top: 3px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.thread {
|
||||
background-color: var(--color-fill-float-hover);
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.comment-form {
|
||||
background-color: var(--color-fill-float-hover);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.comment-header {
|
||||
display: flex;
|
||||
background-color: var(--color-fill-ghost);
|
||||
clip-path: var(--1px-corner-fill);
|
||||
padding: 0 8px;
|
||||
width: fit-content;
|
||||
}
|
||||
</style>
|
||||
|
||||
{#snippet commentHeader(filePath?: string, selectionRange?: SelectionRange)}
|
||||
{#if filePath && selectionRange}
|
||||
<div class="comment-header">
|
||||
{filePath.split("/").length > 1 ? "…/" : ""}{filePath
|
||||
.split("/")
|
||||
.slice(-1)}:{selectionRange.start.side === "left"
|
||||
? "L"
|
||||
: "R"}{selectionRange.start.lineNumber}
|
||||
{#if selectionRange.end}
|
||||
->
|
||||
{selectionRange.end.side === "left" ? "L" : "R"}{selectionRange.end
|
||||
.lineNumber}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<div class="container">
|
||||
{#if file.diff.type === "plain"}
|
||||
{#each file.diff.hunks as hunk, hunkIdx}
|
||||
<div class="line hunk-header">
|
||||
<div class="left"></div>
|
||||
<div class="right"></div>
|
||||
<div class="sign"></div>
|
||||
<div class="code">{hunk.header}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#each hunk.lines as line, lineIdx}
|
||||
{@const thread = findLineThread(line)}
|
||||
<div
|
||||
class="line"
|
||||
class:addition={line.type === "addition"}
|
||||
class:deletion={line.type === "deletion"}
|
||||
class:context={line.type === "context"}
|
||||
class:selected={!thread &&
|
||||
isSelected(filePath(file, "left"), hunkIdx, lineIdx)}>
|
||||
<div
|
||||
class="left"
|
||||
class:marker={selection?.start.side === "left" &&
|
||||
selection.start.lineNumber === lineNumber(line, "left")}
|
||||
onpointerdown={e => {
|
||||
if (codeComments?.createComment && !thread) {
|
||||
selectLine(e, file, "left", line, hunkIdx, lineIdx);
|
||||
}
|
||||
}}>
|
||||
{lineNumber(line, "left")}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="right"
|
||||
class:marker={selection?.start.side === "right" &&
|
||||
selection.start.lineNumber === lineNumber(line, "right")}
|
||||
onpointerdown={e => {
|
||||
if (codeComments?.createComment && !thread) {
|
||||
selectLine(e, file, "right", line, hunkIdx, lineIdx);
|
||||
}
|
||||
}}>
|
||||
{lineNumber(line, "right")}
|
||||
</div>
|
||||
|
||||
<div class="sign">
|
||||
{#if line.type === "addition"}
|
||||
+
|
||||
{:else if line.type === "deletion"}
|
||||
-
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if line.highlight && line.highlight.items.length > 0}
|
||||
<div class="code">
|
||||
{@html line.highlight.items
|
||||
.map(
|
||||
paint =>
|
||||
`<span class="global-syntax ${paint.style}">${escape(paint.item)}</span>`,
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
{:else if line.line !== ""}
|
||||
<div class="code">{line.line}</div>
|
||||
{:else}
|
||||
<div class="code"><br /></div>
|
||||
{/if}
|
||||
|
||||
<div class="global-flex comment-icon">
|
||||
{#if thread}
|
||||
{#if thread.root.resolved && thread.replies.every(r => r.resolved)}
|
||||
<Icon name="comment-checkmark" />
|
||||
{:else}
|
||||
<Icon name="comment" />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if codeComments && thread}
|
||||
<div class="thread">
|
||||
<div style:padding="0.5rem">
|
||||
{@render commentHeader(
|
||||
thread.root.location?.path,
|
||||
rangeAnchorsFromCodeLocation(thread.root.location),
|
||||
)}
|
||||
</div>
|
||||
<ThreadComponent
|
||||
inline
|
||||
rid={codeComments.rid}
|
||||
{thread}
|
||||
reactOnComment={codeComments.config &&
|
||||
partial(
|
||||
codeComments.reactOnComment,
|
||||
codeComments.config.publicKey,
|
||||
)}
|
||||
createReply={async (body, embeds) => {
|
||||
await codeComments.createComment(
|
||||
body,
|
||||
embeds,
|
||||
thread.root.id,
|
||||
);
|
||||
}}
|
||||
editComment={codeComments.editComment}
|
||||
canEditComment={partial(
|
||||
roles.isDelegateOrAuthor,
|
||||
codeComments.config.publicKey,
|
||||
codeComments.repoDelegates.map(delegate => delegate.did),
|
||||
)} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if codeComments && selection && selection.hunkIdx === hunkIdx && selection.lineIdx === lineIdx && selection.codeLocation}
|
||||
<div
|
||||
class="comment-form"
|
||||
onpointerdown={e => {
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<div style:margin-bottom="1rem">
|
||||
{@render commentHeader(selection.file, {
|
||||
start: selection.start,
|
||||
})}
|
||||
</div>
|
||||
<CommentToggleInput
|
||||
disallowEmptyBody
|
||||
rid={codeComments.rid}
|
||||
onclose={() => {
|
||||
selection = undefined;
|
||||
}}
|
||||
focus
|
||||
placeholder="Leave a comment"
|
||||
submit={async (body, embeds) => {
|
||||
if (selection?.codeLocation) {
|
||||
try {
|
||||
await codeComments.createComment(
|
||||
body,
|
||||
embeds,
|
||||
undefined,
|
||||
selection.codeLocation,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Comment creation failed", e);
|
||||
} finally {
|
||||
selection = undefined;
|
||||
}
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -207,6 +207,7 @@
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
font-family: var(--font-family-sans-serif);
|
||||
}
|
||||
.inline {
|
||||
border: 0;
|
||||
|
||||
@@ -7,14 +7,21 @@
|
||||
import NakedButton from "./NakedButton.svelte";
|
||||
|
||||
interface Props {
|
||||
sticky?: boolean;
|
||||
leftHeader: Snippet;
|
||||
children: Snippet;
|
||||
expanded: boolean;
|
||||
leftHeader: Snippet;
|
||||
rightHeader?: Snippet;
|
||||
sticky?: boolean;
|
||||
}
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
let { sticky = true, leftHeader, children, expanded }: Props = $props();
|
||||
let {
|
||||
children,
|
||||
expanded,
|
||||
leftHeader,
|
||||
rightHeader,
|
||||
sticky = true,
|
||||
}: Props = $props();
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
let header: HTMLElement | undefined = $state();
|
||||
@@ -91,6 +98,15 @@
|
||||
</NakedButton>
|
||||
{@render leftHeader()}
|
||||
</div>
|
||||
{#if rightHeader}
|
||||
<div
|
||||
class="global-flex"
|
||||
style:gap="1rem"
|
||||
style:margin-left="auto"
|
||||
style:margin-right="1rem">
|
||||
{@render rightHeader()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if expanded}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import type { FileDiff } from "@bindings/diff/FileDiff";
|
||||
import type { CodeComments } from "./Diff.svelte";
|
||||
|
||||
import Diff from "@app/components/Diff.svelte";
|
||||
import File from "@app/components/File.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import Path from "@app/components/Path.svelte";
|
||||
|
||||
interface Props {
|
||||
expanded: boolean;
|
||||
file: FileDiff;
|
||||
head: string;
|
||||
codeComments?: CodeComments;
|
||||
}
|
||||
|
||||
const { expanded, file, head, codeComments }: Props = $props();
|
||||
|
||||
// Pass down only the comments that apply to the given diff.
|
||||
function filterThreadsByFilePath() {
|
||||
if (codeComments) {
|
||||
if (
|
||||
file.status === "added" ||
|
||||
file.status === "deleted" ||
|
||||
file.status === "modified"
|
||||
) {
|
||||
return {
|
||||
...codeComments,
|
||||
threads:
|
||||
codeComments.threads.filter(t => {
|
||||
return t.root.location?.path === file.path;
|
||||
}) ?? [],
|
||||
};
|
||||
} else {
|
||||
return { ...codeComments, threads: [] };
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const commentsOfThisFile = $derived(filterThreadsByFilePath());
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.added {
|
||||
color: var(--color-foreground-success);
|
||||
background-color: var(--color-fill-diff-green-light);
|
||||
}
|
||||
.deleted {
|
||||
color: var(--color-foreground-red);
|
||||
background-color: var(--color-fill-diff-red-light);
|
||||
}
|
||||
.moved,
|
||||
.copied {
|
||||
color: var(--color-foreground-dim);
|
||||
background: var(--color-fill-ghost);
|
||||
}
|
||||
.stats {
|
||||
font-size: var(--font-size-tiny);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
</style>
|
||||
|
||||
{#snippet emptyPlaceholder()}
|
||||
<div class="global-flex" style:margin="1rem 0" style:justify-content="center">
|
||||
<Icon name="none" />Empty file
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<File {expanded}>
|
||||
{#snippet leftHeader()}
|
||||
{#if file.status === "moved" || file.status === "copied"}
|
||||
<span style="display: flex; align-items: center; flex-wrap: wrap;">
|
||||
<Path fullPath={file.oldPath} />
|
||||
<span style:padding="0 0.5rem">→</span>
|
||||
<Path fullPath={file.newPath} />
|
||||
</span>
|
||||
{:else}
|
||||
<Path fullPath={file.path} />
|
||||
{/if}
|
||||
|
||||
{#if file.status === "added"}
|
||||
<span class="global-counter added">added</span>
|
||||
{:else if file.status === "deleted"}
|
||||
<span class="global-counter deleted">deleted</span>
|
||||
{:else if file.status === "moved"}
|
||||
<span class="global-counter moved">moved</span>
|
||||
{:else if file.status === "copied"}
|
||||
<span class="global-counter copied">copied</span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet rightHeader()}
|
||||
{#if file.diff.type === "plain" && file.diff.hunks.length > 0}
|
||||
<div class="stats">
|
||||
<span style:color="var(--color-foreground-success)">
|
||||
+{file.diff.stats.additions}
|
||||
</span>
|
||||
<span style:color="var(--color-foreground-red)">
|
||||
-{file.diff.stats.deletions}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if commentsOfThisFile && commentsOfThisFile.threads.length > 0}
|
||||
<Icon name="comment" />
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if file.diff.type === "plain"}
|
||||
{#if file.diff.hunks.length > 0}
|
||||
<Diff {file} {head} codeComments={commentsOfThisFile} />
|
||||
{:else}
|
||||
{@render emptyPlaceholder()}
|
||||
{/if}
|
||||
{:else if file.diff.type === "binary"}
|
||||
<div
|
||||
class="global-flex"
|
||||
style:margin="1rem 0"
|
||||
style:justify-content="center">
|
||||
<Icon name="binary" />Binary file
|
||||
</div>
|
||||
{:else}
|
||||
{@render emptyPlaceholder()}
|
||||
{/if}
|
||||
</File>
|
||||
@@ -12,6 +12,7 @@
|
||||
| "arrow-right"
|
||||
| "arrow-right-hollow"
|
||||
| "attachment"
|
||||
| "binary"
|
||||
| "branch"
|
||||
| "broom"
|
||||
| "broom-double"
|
||||
@@ -162,6 +163,26 @@
|
||||
<path d="M4 8H5V9L4 9V8Z" />
|
||||
<path d="M5 6H11V7H5V6Z" />
|
||||
<path d="M4 7H5L5 8H4L4 7Z" />
|
||||
{:else if name === "binary"}
|
||||
<path d="M10 3.5H11V4.5H10V3.5Z" />
|
||||
<path d="M11 4.5L12 4.5V5.5H11V4.5Z" />
|
||||
<path d="M10 5.5L12 5.5V6.5H10V5.5Z" />
|
||||
<path d="M9 5.5H10V6.5H9V5.5Z" />
|
||||
<path d="M8 4.5H9V5.5L8 5.5V4.5Z" />
|
||||
<path d="M8 2.5H9L9 4.5H8L8 2.5Z" />
|
||||
<path d="M9 2.5L10 2.5V3.5H9V2.5Z" />
|
||||
<path d="M4 13.5H12V14.5H4V13.5Z" />
|
||||
<path d="M4 1.5H9V2.5L4 2.5V1.5Z" />
|
||||
<path d="M13 5.5V13.5L12 13.5L12 5.5L13 5.5Z" />
|
||||
<path d="M4 2.5L4 13.5H3L3 2.5L4 2.5Z" />
|
||||
<path d="M5 9.5H6V11.5H5V9.5Z" />
|
||||
<path d="M7 9.5H8V11.5H7V9.5Z" />
|
||||
<path d="M6 11.5H7L7 12.5H6L6 11.5Z" />
|
||||
<path d="M6 8.5H7V9.5L6 9.5L6 8.5Z" />
|
||||
<path d="M9 9.5H10V10.5H9V9.5Z" />
|
||||
<path d="M10 8.5H11V12.5H10V8.5Z" />
|
||||
<path d="M5 4.5H6V5.5H5V4.5Z" />
|
||||
<path d="M6 3.5H7V7.5H6V3.5Z" />
|
||||
{:else if name === "branch"}
|
||||
<path d="M11 5L10 5V2L13 2V5L12 5V8L11 8V5ZM11 3H12V4H11V3Z" />
|
||||
<path
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
fullPath: string;
|
||||
}
|
||||
|
||||
const { fullPath }: Props = $props();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
white-space: nowrap;
|
||||
-webkit-touch-callout: initial;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
.path {
|
||||
color: var(--color-fill-gray);
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
.filename {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<span class="txt-small container">
|
||||
<span class="path">
|
||||
{fullPath
|
||||
.match(/^.*\/|/)
|
||||
?.values()
|
||||
.next().value}
|
||||
</span><span class="filename">
|
||||
{fullPath.split("/").slice(-1)}
|
||||
</span>
|
||||
</span>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Author } from "@bindings/cob/Author";
|
||||
import type { CodeLocation } from "@bindings/cob/thread/CodeLocation";
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
import type { Embed } from "@bindings/cob/thread/Embed";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
@@ -66,7 +67,9 @@
|
||||
review.comments
|
||||
.filter(
|
||||
comment =>
|
||||
(comment.id !== review.id && !comment.replyTo) ||
|
||||
(!comment.location &&
|
||||
comment.id !== review.id &&
|
||||
!comment.replyTo) ||
|
||||
comment.replyTo === review.id,
|
||||
)
|
||||
.map(thread => {
|
||||
@@ -81,6 +84,28 @@
|
||||
}, [])) as Thread[]) || [],
|
||||
);
|
||||
|
||||
const codeCommentThreads = $derived(
|
||||
((review.comments &&
|
||||
review.comments
|
||||
.filter(
|
||||
comment =>
|
||||
(comment.location &&
|
||||
comment.id !== review.id &&
|
||||
!comment.replyTo) ||
|
||||
comment.replyTo === review.id,
|
||||
)
|
||||
.map(thread => {
|
||||
return {
|
||||
root: thread,
|
||||
replies:
|
||||
review.comments &&
|
||||
review.comments
|
||||
.filter(comment => comment.replyTo === thread.id)
|
||||
.sort((a, b) => a.edits[0].timestamp - b.edits[0].timestamp),
|
||||
};
|
||||
}, [])) as Thread<CodeLocation>[]) || [],
|
||||
);
|
||||
|
||||
let verdict: Review["verdict"] = $state(review.verdict);
|
||||
let labelSaveInProgress: boolean = $state(false);
|
||||
|
||||
@@ -122,8 +147,8 @@
|
||||
body: string,
|
||||
embeds: Embed[],
|
||||
replyTo?: string,
|
||||
location?: CodeLocation,
|
||||
) {
|
||||
console.log({ replyTo });
|
||||
try {
|
||||
await invoke("edit_patch", {
|
||||
rid: repo.rid,
|
||||
@@ -134,6 +159,7 @@
|
||||
body,
|
||||
embeds,
|
||||
replyTo,
|
||||
location,
|
||||
},
|
||||
opts: { announce: $nodeRunning && $announce },
|
||||
});
|
||||
@@ -350,5 +376,18 @@
|
||||
{editComment}
|
||||
{reactOnComment} />
|
||||
|
||||
<Changes rid={repo.rid} showCommits={false} {patchId} {revision} />
|
||||
<Changes
|
||||
codeComments={{
|
||||
config,
|
||||
createComment,
|
||||
editComment,
|
||||
reactOnComment,
|
||||
repoDelegates: repo.delegates,
|
||||
rid: repo.rid,
|
||||
threads: codeCommentThreads,
|
||||
}}
|
||||
rid={repo.rid}
|
||||
showCommits={false}
|
||||
{patchId}
|
||||
{revision} />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { Author } from "@bindings/cob/Author";
|
||||
import type { Comment } from "@bindings/cob/thread/Comment";
|
||||
import type { CodeLocation } from "@bindings/cob/thread/CodeLocation";
|
||||
import type { Embed } from "@bindings/cob/thread/Embed";
|
||||
import type { Thread } from "@bindings/cob/thread/Thread";
|
||||
|
||||
import { tick } from "svelte";
|
||||
import partial from "lodash/partial";
|
||||
@@ -14,10 +15,7 @@
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
|
||||
interface Props {
|
||||
thread: {
|
||||
root: Comment;
|
||||
replies: Comment[];
|
||||
};
|
||||
thread: Thread<CodeLocation>;
|
||||
rid: string;
|
||||
canEditComment: (author: string) => true | undefined;
|
||||
editComment?: (
|
||||
@@ -35,6 +33,7 @@
|
||||
authors: Author[],
|
||||
reaction: string,
|
||||
) => Promise<void>;
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -44,6 +43,7 @@
|
||||
editComment,
|
||||
createReply,
|
||||
reactOnComment,
|
||||
inline = false,
|
||||
}: Props = $props();
|
||||
|
||||
async function toggleReply() {
|
||||
@@ -76,6 +76,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
font-family: var(--font-family-sans-serif);
|
||||
}
|
||||
|
||||
.top-level-comment {
|
||||
@@ -96,8 +97,53 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
{#snippet repliesSnippet()}
|
||||
<div style:width="100%">
|
||||
{#if replies.length > 0}
|
||||
{#each replies as reply}
|
||||
<CommentComponent
|
||||
disallowEmptyBody
|
||||
{rid}
|
||||
lastEdit={reply.edits.length > 1 ? reply.edits.at(-1) : undefined}
|
||||
id={reply.id}
|
||||
author={reply.author}
|
||||
caption="replied"
|
||||
reactions={reply.reactions}
|
||||
timestamp={reply.edits[0].timestamp}
|
||||
body={reply.edits.slice(-1)[0].body}
|
||||
editComment={editComment &&
|
||||
canEditComment(reply.author.did) &&
|
||||
partial(editComment, reply.id)}
|
||||
reactOnComment={reactOnComment &&
|
||||
partial(reactOnComment, reply.id)} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#if createReply && showReplyForm}
|
||||
<div id={`reply-${root.id}`} style:padding="1rem">
|
||||
<ExtendedTextarea
|
||||
disallowEmptyBody
|
||||
{submitInProgress}
|
||||
{rid}
|
||||
placeholder="Reply to comment"
|
||||
submitCaption="Reply"
|
||||
focus
|
||||
close={() => (showReplyForm = false)}
|
||||
submit={async ({ comment, embeds }) => {
|
||||
try {
|
||||
submitInProgress = true;
|
||||
await createReply(comment, Array.from(embeds.values()), root.id);
|
||||
} finally {
|
||||
showReplyForm = false;
|
||||
submitInProgress = false;
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="comments" {style}>
|
||||
<div class="top-level-comment">
|
||||
<div class:top-level-comment={!inline}>
|
||||
<CommentComponent
|
||||
disallowEmptyBody
|
||||
{rid}
|
||||
@@ -112,58 +158,21 @@
|
||||
partial(editComment, root.id)}
|
||||
reactOnComment={reactOnComment && partial(reactOnComment, root.id)}>
|
||||
{#snippet actions()}
|
||||
<Icon name="reply" onclick={toggleReply} />
|
||||
{#if createReply}
|
||||
<Icon name="reply" onclick={toggleReply} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</CommentComponent>
|
||||
</div>
|
||||
{#if replies.length > 0 || (createReply && showReplyForm)}
|
||||
<Border variant="float" styleOverflow="hidden" flatTop>
|
||||
<div style:width="100%">
|
||||
{#if replies.length > 0}
|
||||
{#each replies as reply}
|
||||
<CommentComponent
|
||||
disallowEmptyBody
|
||||
{rid}
|
||||
lastEdit={reply.edits.length > 1 ? reply.edits.at(-1) : undefined}
|
||||
id={reply.id}
|
||||
author={reply.author}
|
||||
caption="replied"
|
||||
reactions={reply.reactions}
|
||||
timestamp={reply.edits[0].timestamp}
|
||||
body={reply.edits.slice(-1)[0].body}
|
||||
editComment={editComment &&
|
||||
canEditComment(reply.author.did) &&
|
||||
partial(editComment, reply.id)}
|
||||
reactOnComment={reactOnComment &&
|
||||
partial(reactOnComment, reply.id)} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#if createReply && showReplyForm}
|
||||
<div id={`reply-${root.id}`} style:padding="1rem">
|
||||
<ExtendedTextarea
|
||||
disallowEmptyBody
|
||||
{submitInProgress}
|
||||
{rid}
|
||||
placeholder="Reply to comment"
|
||||
submitCaption="Reply"
|
||||
focus
|
||||
close={() => (showReplyForm = false)}
|
||||
submit={async ({ comment, embeds }) => {
|
||||
try {
|
||||
submitInProgress = true;
|
||||
await createReply(
|
||||
comment,
|
||||
Array.from(embeds.values()),
|
||||
root.id,
|
||||
);
|
||||
} finally {
|
||||
showReplyForm = false;
|
||||
submitInProgress = false;
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if inline}
|
||||
<div style:background-color="var(--color-background-float)">
|
||||
{@render repliesSnippet()}
|
||||
</div>
|
||||
</Border>
|
||||
{:else}
|
||||
<Border variant="float" styleOverflow="hidden" flatTop={!inline}>
|
||||
{@render repliesSnippet()}
|
||||
</Border>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -259,6 +259,16 @@
|
||||
console.error("Loading patch list failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
function findReviewRevision(review: Review): Revision {
|
||||
// Every review is guaranteed to have a revision according to the protocol
|
||||
// model, so using type assertions here is safe.
|
||||
return revisions.find(revision => {
|
||||
return revision.reviews!.find(rev => {
|
||||
return rev.id === review.id;
|
||||
});
|
||||
}) as Revision;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -447,7 +457,7 @@
|
||||
{repo}
|
||||
{reload}
|
||||
{review}
|
||||
revision={selectedRevision}
|
||||
revision={findReviewRevision(review)}
|
||||
onNavigateBack={() => {
|
||||
review = undefined;
|
||||
}} />
|
||||
|
||||
Reference in New Issue
Block a user