Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add discussion section to reviews
Merged rudolfs opened 1 year ago

TBD: margin styling to align the discussion section in the Issue and Review views. The rest is ready for review :)

check check-e2e

👉 Workflow runs 👉 Branch on GitHub

8 files changed +370 -274 15bb0353 2e6fed98
modified crates/radicle-types/src/domain/patch/models/patch.rs
@@ -375,6 +375,7 @@ pub enum Action {
        review: patch::ReviewId,
    },
    #[serde(rename = "review.comment")]
+
    #[serde(rename_all = "camelCase")]
    ReviewComment {
        #[ts(as = "String")]
        review: patch::ReviewId,
added src/components/Discussion.svelte
@@ -0,0 +1,167 @@
+
<script lang="ts">
+
  import type { Author } from "@bindings/cob/Author";
+
  import type { Config } from "@bindings/config/Config";
+
  import type { Embed } from "@bindings/cob/thread/Embed";
+
  import type { Thread } from "@bindings/cob/thread/Thread";
+

+
  import partial from "lodash/partial";
+
  import sum from "lodash/sum";
+
  import { tick } from "svelte";
+

+
  import * as roles from "@app/lib/roles";
+
  import { scrollIntoView } from "@app/lib/utils";
+

+
  import CommentToggleInput from "@app/components/CommentToggleInput.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import NakedButton from "./NakedButton.svelte";
+
  import ThreadComponent from "@app/components/Thread.svelte";
+

+
  interface Props {
+
    cobId: string;
+
    commentThreads: Thread[];
+
    config: Config;
+
    createComment: (
+
      body: string,
+
      embeds: Embed[],
+
      replyTo?: string,
+
    ) => 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;
+
  }
+

+
  /* eslint-disable prefer-const */
+
  let {
+
    cobId,
+
    commentThreads,
+
    config,
+
    createComment,
+
    editComment,
+
    reactOnComment,
+
    repoDelegates,
+
    rid,
+
  }: Props = $props();
+
  /* eslint-enable prefer-const */
+

+
  $effect(() => {
+
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+
    cobId;
+

+
    hideDiscussion = commentThreads.length === 0;
+
    focusReply = false;
+
    topLevelReplyOpen = false;
+
  });
+

+
  let focusReply: boolean = $state(false);
+
  let topLevelReplyOpen = $state(false);
+

+
  let hideDiscussion = $state(commentThreads.length === 0);
+

+
  async function toggleReply() {
+
    topLevelReplyOpen = !topLevelReplyOpen;
+
    if (!topLevelReplyOpen) {
+
      return;
+
    }
+

+
    await tick();
+
    scrollIntoView(`reply-${cobId}`, {
+
      behavior: "smooth",
+
      block: "center",
+
    });
+
    await tick();
+
    focusReply = true;
+
  }
+
</script>
+

+
<style>
+
  .connector {
+
    width: 2px;
+
    height: 1rem;
+
    margin-left: 1.25rem;
+
    background-color: var(--color-background-float);
+
  }
+
  .hide {
+
    display: none;
+
  }
+
</style>
+

+
<div style:margin={hideDiscussion ? "1.5rem 0" : "1.5rem 0 2.5rem 0"}>
+
  <div class="global-flex">
+
    <NakedButton
+
      variant="ghost"
+
      disabled={commentThreads.length === 0}
+
      onclick={() => (hideDiscussion = !hideDiscussion)}>
+
      <Icon name={hideDiscussion ? "chevron-right" : "chevron-down"} />
+
      <div class="txt-semibold global-flex txt-regular">
+
        Discussion <span style:font-weight="var(--font-weight-regular)">
+
          {sum(
+
            commentThreads.map(t => {
+
              return t.replies.length + 1;
+
            }),
+
          )}
+
        </span>
+
      </div>
+
    </NakedButton>
+
    <div style:margin-left="auto">
+
      <NakedButton
+
        variant="secondary"
+
        onclick={async () => {
+
          if (hideDiscussion) {
+
            hideDiscussion = false;
+
          } else {
+
            if (commentThreads.length === 0) {
+
              hideDiscussion = true;
+
            }
+
          }
+
          await toggleReply();
+
        }}>
+
        <Icon name="comment" />
+
        <span class="txt-small">Comment</span>
+
      </NakedButton>
+
    </div>
+
  </div>
+
  <div class:hide={hideDiscussion} style:margin-top="1rem">
+
    {#each commentThreads as thread}
+
      <ThreadComponent
+
        {thread}
+
        {rid}
+
        canEditComment={partial(
+
          roles.isDelegateOrAuthor,
+
          config.publicKey,
+
          repoDelegates.map(delegate => delegate.did),
+
        )}
+
        {editComment}
+
        createReply={createComment}
+
        reactOnComment={partial(reactOnComment, config.publicKey)} />
+
      <div class="connector"></div>
+
    {/each}
+

+
    <div id={`reply-${cobId}`}>
+
      <CommentToggleInput
+
        disallowEmptyBody
+
        {rid}
+
        focus={focusReply}
+
        onexpand={toggleReply}
+
        onclose={topLevelReplyOpen
+
          ? () => {
+
              if (commentThreads.length === 0) {
+
                hideDiscussion = !hideDiscussion;
+
              }
+
              topLevelReplyOpen = false;
+
            }
+
          : undefined}
+
        placeholder="Leave a comment"
+
        submit={createComment} />
+
    </div>
+
  </div>
+
</div>
modified src/components/Review.svelte
@@ -1,10 +1,14 @@
<script lang="ts">
+
  import type { Author } from "@bindings/cob/Author";
  import type { Config } from "@bindings/config/Config";
+
  import type { Embed } from "@bindings/cob/thread/Embed";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
  import type { Review } from "@bindings/cob/patch/Review";
  import type { Revision } from "@bindings/cob/patch/Revision";
+
  import type { Thread } from "@bindings/cob/thread/Thread";

  import partial from "lodash/partial";
+
  import uniqBy from "lodash/uniqBy";

  import * as roles from "@app/lib/roles";

@@ -22,6 +26,7 @@
  import NodeId from "@app/components/NodeId.svelte";
  import VerdictButton from "@app/components/VerdictButton.svelte";
  import VerdictBadge from "./VerdictBadge.svelte";
+
  import Discussion from "./Discussion.svelte";

  interface Props {
    config: Config;
@@ -43,12 +48,37 @@
    repo,
  }: Props = $props();

-
  const contributors = [
-
    review.author,
-
    ...review.comments.map(c => {
-
      return c.author;
-
    }),
-
  ];
+
  const contributors = $derived(
+
    uniqBy(
+
      [
+
        review.author,
+
        ...review.comments.map(c => {
+
          return c.author;
+
        }),
+
      ],
+
      "did",
+
    ),
+
  );
+

+
  const commentThreads = $derived(
+
    ((review.comments &&
+
      review.comments
+
        .filter(
+
          comment =>
+
            (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[]) || [],
+
  );

  let verdict: Review["verdict"] = $state(review.verdict);
  let labelSaveInProgress: boolean = $state(false);
@@ -86,6 +116,81 @@
      await reload(reviewId);
    }
  }
+

+
  async function createComment(
+
    body: string,
+
    embeds: Embed[],
+
    replyTo?: string,
+
  ) {
+
    console.log({ replyTo });
+
    try {
+
      await invoke("edit_patch", {
+
        rid: repo.rid,
+
        cobId: patchId,
+
        action: {
+
          type: "review.comment",
+
          review: review.id,
+
          body,
+
          embeds,
+
          replyTo,
+
        },
+
        opts: { announce: $nodeRunning && $announce },
+
      });
+
    } catch (error) {
+
      console.error("Creating comment failed", error);
+
    } finally {
+
      await reload(review.id);
+
    }
+
  }
+

+
  async function editComment(commentId: string, body: string, embeds: Embed[]) {
+
    try {
+
      await invoke("edit_patch", {
+
        rid: repo.rid,
+
        cobId: patchId,
+
        action: {
+
          type: "review.comment.edit",
+
          comment: commentId,
+
          body,
+
          review: review.id,
+
          embeds,
+
        },
+
        opts: { announce: $nodeRunning && $announce },
+
      });
+
    } catch (error) {
+
      console.error("Editing comment failed: ", error);
+
    } finally {
+
      await reload(review.id);
+
    }
+
  }
+

+
  async function reactOnComment(
+
    publicKey: string,
+
    commentId: string,
+
    authors: Author[],
+
    reaction: string,
+
  ) {
+
    try {
+
      await invoke("edit_patch", {
+
        rid: repo.rid,
+
        cobId: patchId,
+
        action: {
+
          type: "review.comment.react",
+
          comment: commentId,
+
          reaction,
+
          review: review.id,
+
          active: !authors.find(
+
            ({ did }) => publicKeyFromDid(did) === publicKey,
+
          ),
+
        },
+
        opts: { announce: $nodeRunning && $announce },
+
      });
+
    } catch (error) {
+
      console.error("Editing comment reactions failed", error);
+
    } finally {
+
      await reload(review.id);
+
    }
+
  }
</script>

<style>
@@ -202,9 +307,11 @@

      <div class="metadata-section" style:flex="1">
        <div class="metadata-section-title">Participants</div>
-
        {#each contributors as contributor}
-
          <NodeId {...authorForNodeId(contributor)} />
-
        {/each}
+
        <div class="global-flex">
+
          {#each contributors as contributor}
+
            <NodeId {...authorForNodeId(contributor)} />
+
          {/each}
+
        </div>
      </div>
    </Border>

@@ -231,4 +338,14 @@
      </CommentComponent>
    </div>
  </div>
+

+
  <Discussion
+
    cobId={patchId}
+
    repoDelegates={repo.delegates}
+
    rid={repo.rid}
+
    {commentThreads}
+
    {config}
+
    {createComment}
+
    {editComment}
+
    {reactOnComment} />
</div>
modified src/components/ReviewTeaser.svelte
@@ -119,18 +119,21 @@
          {formatTimestamp(review.timestamp)}
        </div>
      </div>
-
      {#if review.comments.length > 0}
-
        <div class="global-flex" style:gap="0.25rem" style:margin-left="auto">
-
          <Icon name="comment" />{review.comments.length}
-
        </div>
-
      {/if}
-
      {#if review.labels.length > 0}
-
        <div class="global-flex" style:margin-left="auto">
-
          {#each review.labels as label}
-
            <Label {label} />
-
          {/each}
-
        </div>
-
      {/if}
+

+
      <div class="global-flex" style:gap="1rem">
+
        {#if review.labels.length > 0}
+
          <div class="global-flex" style:margin-left="auto">
+
            {#each review.labels as label}
+
              <Label {label} />
+
            {/each}
+
          </div>
+
        {/if}
+
        {#if review.comments.length > 0}
+
          <div class="global-flex" style:gap="0.25rem" style:margin-left="auto">
+
            <Icon name="comment" />{review.comments.length}
+
          </div>
+
        {/if}
+
      </div>
    </div>
    {#if review.summary?.trim()}
      <div>
modified src/components/Revision.svelte
@@ -10,29 +10,23 @@
  import type { Verdict } from "@bindings/cob/patch/Verdict";

  import partial from "lodash/partial";
-
  import { tick } from "svelte";

  import * as roles from "@app/lib/roles";
  import { announce } from "@app/components/AnnounceSwitch.svelte";
  import { invoke } from "@app/lib/invoke";
  import { nodeRunning } from "@app/lib/events";
-
  import {
-
    didFromPublicKey,
-
    publicKeyFromDid,
-
    scrollIntoView,
-
  } from "@app/lib/utils";
+
  import { didFromPublicKey, publicKeyFromDid } from "@app/lib/utils";

  import Button from "@app/components/Button.svelte";
  import Changeset from "@app/components/Changeset.svelte";
  import CobCommitTeaser from "./CobCommitTeaser.svelte";
  import CommentComponent from "@app/components/Comment.svelte";
-
  import CommentToggleInput from "@app/components/CommentToggleInput.svelte";
  import CommitsContainer from "@app/components/CommitsContainer.svelte";
+
  import Discussion from "./Discussion.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Id from "./Id.svelte";
  import NakedButton from "./NakedButton.svelte";
  import ReviewTeaser from "@app/components/ReviewTeaser.svelte";
-
  import ThreadComponent from "@app/components/Thread.svelte";

  interface Props {
    rid: string;
@@ -58,18 +52,22 @@
    ),
  );

-
  let focusReply: boolean = $state(false);
  let hideChanges = $state(false);
-
  let hideDiscussion = $state(
-
    revision.discussion === undefined || revision.discussion.length === 0,
-
  );
  let hideReviews = $state(
    revision.reviews === undefined || revision.reviews.length === 0,
  );
-
  let topLevelReplyOpen = $state(false);
  let filesExpanded = $state(true);

-
  const threads = $derived(
+
  $effect(() => {
+
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+
    patchId;
+

+
    hideReviews =
+
      revision.reviews === undefined || revision.reviews.length === 0;
+
    hideChanges = false;
+
  });
+

+
  const commentThreads = $derived(
    ((revision.discussion &&
      revision.discussion
        .filter(
@@ -89,19 +87,6 @@
        }, [])) as Thread[]) || [],
  );

-
  $effect(() => {
-
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
-
    patchId;
-

-
    hideReviews =
-
      revision.reviews === undefined || revision.reviews.length === 0;
-
    hideDiscussion =
-
      revision.discussion === undefined || revision.discussion.length === 0;
-
    focusReply = false;
-
    topLevelReplyOpen = false;
-
    hideChanges = false;
-
  });
-

  async function editRevision(
    revisionId: string,
    description: string,
@@ -175,6 +160,24 @@
    }
  }

+
  async function createComment(
+
    body: string,
+
    embeds: Embed[],
+
    replyTo?: string,
+
  ) {
+
    try {
+
      await invoke("create_patch_comment", {
+
        rid: rid,
+
        new: { id: patchId, body, embeds, replyTo, revision: revision.id },
+
        opts: { announce: $nodeRunning && $announce },
+
      });
+
    } catch (error) {
+
      console.error("Creating comment failed", error);
+
    } finally {
+
      await reload();
+
    }
+
  }
+

  async function editComment(commentId: string, body: string, embeds: Embed[]) {
    try {
      await invoke("edit_patch", {
@@ -196,20 +199,6 @@
    }
  }

-
  async function createReply(replyTo: string, body: string, embeds: Embed[]) {
-
    try {
-
      await invoke("create_patch_comment", {
-
        rid: rid,
-
        new: { id: patchId, body, embeds, replyTo, revision: revision.id },
-
        opts: { announce: $nodeRunning && $announce },
-
      });
-
    } catch (error) {
-
      console.error("Creating reply failed", error);
-
    } finally {
-
      await reload();
-
    }
-
  }
-

  async function reactOnComment(
    publicKey: string,
    commentId: string,
@@ -238,35 +227,6 @@
    }
  }

-
  async function createComment(body: string, embeds: Embed[]) {
-
    try {
-
      await invoke("create_patch_comment", {
-
        rid: rid,
-
        new: { id: patchId, body, embeds, revision: revision.id },
-
        opts: { announce: $nodeRunning && $announce },
-
      });
-
    } catch (error) {
-
      console.error("Creating comment failed: ", error);
-
    } finally {
-
      await reload();
-
    }
-
  }
-

-
  async function toggleReply() {
-
    topLevelReplyOpen = !topLevelReplyOpen;
-
    if (!topLevelReplyOpen) {
-
      return;
-
    }
-

-
    await tick();
-
    scrollIntoView(`reply-${patchId}`, {
-
      behavior: "smooth",
-
      block: "center",
-
    });
-
    await tick();
-
    focusReply = true;
-
  }
-

  async function loadHighlightedDiff(rid: string, base: string, head: string) {
    return invoke<Diff>("get_diff", {
      rid,
@@ -309,12 +269,6 @@
  .hide {
    display: none;
  }
-
  .connector {
-
    width: 2px;
-
    height: 1rem;
-
    margin-left: 1.25rem;
-
    background-color: var(--color-background-float);
-
  }
  .commits {
    position: relative;
    display: flex;
@@ -415,79 +369,15 @@
  {/if}
</div>

-
<div style:margin={hideDiscussion ? "1.5rem 0" : "0 0 2.5rem 0"}>
-
  <div class="global-flex">
-
    <NakedButton
-
      variant="ghost"
-
      disabled={revision.discussion === undefined ||
-
        revision.discussion.length === 0}
-
      onclick={() => (hideDiscussion = !hideDiscussion)}>
-
      <Icon name={hideDiscussion ? "chevron-right" : "chevron-down"} />
-
      <div class="txt-semibold global-flex txt-regular">
-
        Discussion <span style:font-weight="var(--font-weight-regular)">
-
          {revision.discussion?.length ?? 0}
-
        </span>
-
      </div>
-
    </NakedButton>
-
    <div style:margin-left="auto">
-
      <NakedButton
-
        variant="secondary"
-
        onclick={async () => {
-
          if (hideDiscussion) {
-
            hideDiscussion = false;
-
          } else {
-
            if (
-
              revision.discussion === undefined ||
-
              revision.discussion.length === 0
-
            ) {
-
              hideDiscussion = true;
-
            }
-
          }
-
          await toggleReply();
-
        }}>
-
        <Icon name="comment" />
-
        <span class="txt-small">Comment</span>
-
      </NakedButton>
-
    </div>
-
  </div>
-
  <div class:hide={hideDiscussion} style:margin-top="1rem">
-
    {#each threads as thread}
-
      <ThreadComponent
-
        {thread}
-
        {rid}
-
        canEditComment={partial(
-
          roles.isDelegateOrAuthor,
-
          config.publicKey,
-
          repoDelegates.map(delegate => delegate.did),
-
        )}
-
        editComment={partial(editComment)}
-
        createReply={partial(createReply)}
-
        reactOnComment={partial(reactOnComment, config.publicKey)} />
-
      <div class="connector"></div>
-
    {/each}
-

-
    <div id={`reply-${patchId}`}>
-
      <CommentToggleInput
-
        disallowEmptyBody
-
        {rid}
-
        focus={focusReply}
-
        onexpand={toggleReply}
-
        onclose={topLevelReplyOpen
-
          ? () => {
-
              if (
-
                revision.discussion === undefined ||
-
                revision.discussion.length === 0
-
              ) {
-
                hideDiscussion = !hideDiscussion;
-
              }
-
              topLevelReplyOpen = false;
-
            }
-
          : undefined}
-
        placeholder="Leave a comment"
-
        submit={partial(createComment)} />
-
    </div>
-
  </div>
-
</div>
+
<Discussion
+
  cobId={patchId}
+
  {commentThreads}
+
  {config}
+
  {createComment}
+
  {editComment}
+
  {reactOnComment}
+
  {repoDelegates}
+
  {rid} />

<div
  class="txt-semibold global-flex"
modified src/components/Thread.svelte
@@ -26,9 +26,9 @@
      embeds: Embed[],
    ) => Promise<void>;
    createReply?: (
-
      commentId: string,
      comment: string,
      embeds: Embed[],
+
      commentId: string,
    ) => Promise<void>;
    reactOnComment?: (
      commentId: string,
@@ -154,9 +154,9 @@
                try {
                  submitInProgress = true;
                  await createReply(
-
                    root.id,
                    comment,
                    Array.from(embeds.values()),
+
                    root.id,
                  );
                } finally {
                  showReplyForm = false;
modified src/views/repo/Issue.svelte
@@ -4,13 +4,12 @@
  import type { Config } from "@bindings/config/Config";
  import type { Embed } from "@bindings/cob/thread/Embed";
  import type { Issue } from "@bindings/cob/issue/Issue";
+
  import type { IssueStatus } from "./router";
  import type { Operation } from "@bindings/cob/Operation";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
  import type { Thread } from "@bindings/cob/thread/Thread";
-
  import type { IssueStatus } from "./router";

  import partial from "lodash/partial";
-
  import { tick } from "svelte";

  import * as roles from "@app/lib/roles";
  import { invoke } from "@app/lib/invoke";
@@ -19,7 +18,6 @@
    issueStatusBackgroundColor,
    issueStatusColor,
    publicKeyFromDid,
-
    scrollIntoView,
  } from "@app/lib/utils";

  import { announce } from "@app/components/AnnounceSwitch.svelte";
@@ -27,7 +25,6 @@
  import AssigneeInput from "@app/components/AssigneeInput.svelte";
  import Border from "@app/components/Border.svelte";
  import CommentComponent from "@app/components/Comment.svelte";
-
  import CommentToggleInput from "@app/components/CommentToggleInput.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
  import Icon from "@app/components/Icon.svelte";
  import InlineTitle from "@app/components/InlineTitle.svelte";
@@ -38,9 +35,9 @@
  import LabelInput from "@app/components/LabelInput.svelte";
  import Sidebar from "@app/components/Sidebar.svelte";
  import TextInput from "@app/components/TextInput.svelte";
-
  import ThreadComponent from "@app/components/Thread.svelte";

  import Layout from "./Layout.svelte";
+
  import Discussion from "@app/components/Discussion.svelte";

  interface Props {
    repo: RepoInfo;
@@ -66,13 +63,10 @@

  let issues = $state(initialIssues);
  let status = $state(initialStatus);
-
  let topLevelReplyOpen = $state(false);
  let editingTitle = $state(false);
  let updatedTitle = $state("");
  let labelSaveInProgress: boolean = $state(false);
  let assigneesSaveInProgress: boolean = $state(false);
-
  let focusReply: boolean = $state(false);
-
  let hideDiscussion = $state(false);
  let hideTimeline = $state(false);

  $effect(() => {
@@ -84,11 +78,8 @@
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
    issue.id;

-
    topLevelReplyOpen = false;
    editingTitle = false;
    updatedTitle = issue.title;
-
    focusReply = false;
-
    hideDiscussion = false;
    hideTimeline = false;
  });

@@ -146,20 +137,6 @@
    }
  }

-
  async function toggleReply() {
-
    topLevelReplyOpen = !topLevelReplyOpen;
-
    if (!topLevelReplyOpen) {
-
      return;
-
    }
-

-
    await tick();
-
    scrollIntoView(`reply-${issue.id}`, {
-
      behavior: "smooth",
-
      block: "center",
-
    });
-
    focusReply = true;
-
  }
-

  async function reload() {
    [issue, activity, threads] = await Promise.all([
      invoke<Issue>("issue_by_id", {
@@ -176,31 +153,14 @@
      }),
    ]);

-
    topLevelReplyOpen = false;
    editingTitle = false;
  }

-
  async function createComment(body: string, embeds: Embed[]) {
-
    try {
-
      await invoke("create_issue_comment", {
-
        rid: repo.rid,
-
        new: { id: issue.id, body, embeds },
-
        opts: { announce: $nodeRunning && $announce },
-
      });
-
      // Update second column issue comment count without reloading the whole
-
      // issue list.
-
      const issueIndex = issues.findIndex(i => i.id === issue.id);
-
      if (issueIndex !== -1) {
-
        issues[issueIndex].commentCount += 1;
-
      }
-
    } catch (error) {
-
      console.error("Comment creation failed: ", error);
-
    } finally {
-
      await reload();
-
    }
-
  }
-

-
  async function createReply(replyTo: string, body: string, embeds: Embed[]) {
+
  async function createComment(
+
    body: string,
+
    embeds: Embed[],
+
    replyTo?: string,
+
  ) {
    try {
      await invoke("create_issue_comment", {
        rid: repo.rid,
@@ -214,7 +174,7 @@
        issues[issueIndex].commentCount += 1;
      }
    } catch (error) {
-
      console.error("Comment reply creation failed", error);
+
      console.error("Comment creation failed: ", error);
    } finally {
      await reload();
    }
@@ -358,12 +318,6 @@
  .content {
    padding: 1rem 1rem 1rem 0;
  }
-
  .connector {
-
    width: 2px;
-
    height: 1rem;
-
    margin-left: 1.25rem;
-
    background-color: var(--color-background-float);
-
  }
  .title-icons {
    display: flex;
    gap: 1rem;
@@ -545,54 +499,18 @@
          config.publicKey,
          issue.body.id,
        )}>
-
        {#snippet actions()}
-
          <Icon name="reply" onclick={toggleReply} />
-
        {/snippet}
      </CommentComponent>
    </div>

-
    <div style:margin-bottom="1rem">
-
      <!-- svelte-ignore a11y_click_events_have_key_events -->
-
      <div
-
        role="button"
-
        tabindex="0"
-
        class="txt-semibold global-flex"
-
        style:margin-bottom="1rem"
-
        style:cursor="pointer"
-
        onclick={() => (hideDiscussion = !hideDiscussion)}>
-
        <Icon
-
          name={hideDiscussion ? "chevron-right" : "chevron-down"} />Discussion
-
      </div>
-
      <div class:hide={hideDiscussion}>
-
        {#each threads as thread}
-
          <ThreadComponent
-
            {thread}
-
            rid={repo.rid}
-
            canEditComment={partial(
-
              roles.isDelegateOrAuthor,
-
              config.publicKey,
-
              repo.delegates.map(delegate => delegate.did),
-
            )}
-
            {editComment}
-
            createReply={partial(createReply)}
-
            reactOnComment={partial(reactOnComment, config.publicKey)} />
-
          <div class="connector"></div>
-
        {/each}
-

-
        <div id={`reply-${issue.id}`}>
-
          <CommentToggleInput
-
            disallowEmptyBody
-
            rid={repo.rid}
-
            focus={focusReply}
-
            onexpand={toggleReply}
-
            onclose={topLevelReplyOpen
-
              ? () => (topLevelReplyOpen = false)
-
              : undefined}
-
            placeholder="Leave a comment"
-
            submit={partial(createComment)} />
-
        </div>
-
      </div>
-
    </div>
+
    <Discussion
+
      cobId={issue.id}
+
      commentThreads={threads}
+
      {config}
+
      {createComment}
+
      {editComment}
+
      {reactOnComment}
+
      repoDelegates={repo.delegates}
+
      rid={repo.rid} />

    <div>
      <!-- svelte-ignore a11y_click_events_have_key_events -->
modified tests/e2e/repo/issue.spec.ts
@@ -56,7 +56,7 @@ test("creation of top level comments", async ({ page }) => {
      .last(),
  ).toBeVisible();

-
  await page.getByRole("button", { name: "Leave a comment" }).click();
+
  await page.getByRole("button", { name: "icon-comment Comment" }).click();
  await page
    .getByPlaceholder("Leave a comment")
    .fill("A top level comment by playwright");
@@ -67,7 +67,7 @@ test("creation of top level comments", async ({ page }) => {

  await page.getByLabel("icon-reply").first().click();
  await page
-
    .getByPlaceholder("Leave a comment")
+
    .getByPlaceholder("Reply to comment")
    .fill(
      "A top level comment by playwright created by replying to the issue body",
    );
@@ -78,7 +78,7 @@ test("creation of top level comments", async ({ page }) => {
    ),
  ).toBeVisible();

-
  await page.getByLabel("icon-reply").nth(1).click();
+
  await page.getByLabel("icon-reply").click();
  await page
    .getByPlaceholder("Reply to comment")
    .fill("A reply comment by playwright replying to the first comment");