Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add patch timeline
Merged rudolfs opened 1 year ago
4 files changed +487 -41 432db1ce 7aebf953
modified src/components/Icon.svelte
@@ -8,14 +8,17 @@
    styleDisplay?: string;
    styleVerticalAlign?: string;
    name:
-
      | "attachment"
      | "arrow-left"
      | "arrow-right"
+
      | "arrow-right-hollow"
+
      | "attachment"
      | "checkmark"
      | "chevron-down"
      | "chevron-right"
      | "collapse-panel"
      | "comment"
+
      | "comment-checkmark"
+
      | "comment-cross"
      | "copy"
      | "cross"
      | "dashboard"
@@ -28,8 +31,10 @@
      | "home"
      | "inbox"
      | "issue"
+
      | "label"
      | "lock"
      | "markdown"
+
      | "merge"
      | "moon"
      | "more-vertical"
      | "none"
@@ -45,6 +50,7 @@
      | "seedling-filled"
      | "settings"
      | "sun"
+
      | "user"
      | "warning";
  }

@@ -115,6 +121,24 @@
    <path d="M10 3H9.00003V13H10V3Z" />
    <path d="M13 6H12V7H13V6Z" />
    <path d="M14 7H13V8H14V7Z" />
+
  {:else if name === "arrow-right-hollow"}
+
    <path d="M9 9L3 9L3 10L9 10L9 9Z" />
+
    <path d="M9 6L3 6L3 7L9 7L9 6Z" />
+
    <path d="M11 4L10 4L10 5L11 5L11 4Z" />
+
    <path d="M10 4L9 4L9 5L10 5L10 4Z" />
+
    <path d="M10 3L9 3L9 6L10 6L10 3Z" />
+
    <path d="M12 5L11 5L11 6L12 6L12 5Z" />
+
    <path d="M13 6L12 6L12 7L13 7L13 6Z" />
+
    <path d="M13 9L12 9L12 10L13 10L13 9Z" />
+
    <path d="M14 8L13 8L13 9L14 9L14 8Z" />
+
    <path d="M14 7L13 7L13 8L14 8L14 7Z" />
+
    <path d="M12 10L11 10L11 11L12 11L12 10Z" />
+
    <path d="M13 9L12 9L12 10L13 10L13 9Z" />
+
    <path d="M11 11L10 11L10 12L11 12L11 11Z" />
+
    <path d="M12 10L11 10L11 11L12 11L12 10Z" />
+
    <path d="M10 11L9 11L9 12L10 12L10 11Z" />
+
    <path d="M10 10L9 10L9 13L10 13L10 10Z" />
+
    <path d="M2 6L3 6L3 10L2 10L2 6Z" />
  {:else if name === "attachment"}
    <path d="M4 4H12V5H4V4Z" />
    <path d="M4 11H11V12H4V11Z" />
@@ -188,6 +212,72 @@
    <path d="M2 4L3 4L3 13H2V4Z" />
    <path d="M5 5H11V6H5V5Z" />
    <path d="M5 8H11V9H5V8Z" />
+
  {:else if name === "comment-checkmark"}
+
    <path d="M4 2L10 2L10 3L4 3L4 2Z" />
+
    <path d="M3 3L4 3L4 4L3 4L3 3Z" />
+
    <path d="M12 2L13 2L13 3L12 3L12 2Z" />
+
    <path d="M13 2L14 2L14 3L13 3L13 2Z" />
+
    <path d="M12 10L13 10L13 11L12 11L12 10Z" />
+
    <path d="M5 11L12 11L12 12L5 12L5 11Z" />
+
    <path d="M3 13L4 13L4 14L3 14L3 13Z" />
+
    <path d="M4 12L5 12L5 13L4 13L4 12Z" />
+
    <path d="M5 11L6 11L6 12L5 12L5 11Z" />
+
    <path d="M13 5L14 5L14 10L13 10L13 5Z" />
+
    <path d="M2 8L3 8L3 13L2 13L2 8Z" />
+
    <path d="M5 6L6 6L6 7L5 7L5 6Z" />
+
    <path d="M4 5L5 5L5 6L4 6L4 5Z" />
+
    <path d="M6 7L7 7L7 8L6 8L6 7Z" />
+
    <path d="M7 8L8 8L8 9L7 9L7 8Z" />
+
    <path d="M8 7L9 7L9 8L8 8L8 7Z" />
+
    <path d="M9 6L10 6L10 7L9 7L9 6Z" />
+
    <path d="M10 5L11 5L11 6L10 6L10 5Z" />
+
    <path d="M11 4L12 4L12 5L11 5L11 4Z" />
+
    <path d="M12 3L13 3L13 4L12 4L12 3Z" />
+
    <path d="M11 3L12 3L12 4L11 4L11 3Z" />
+
    <path d="M10 4L11 4L11 5L10 5L10 4Z" />
+
    <path d="M9 5L10 5L10 6L9 6L9 5Z" />
+
    <path d="M8 6L9 6L9 7L8 7L8 6Z" />
+
    <path d="M7 7L8 7L8 8L7 8L7 7Z" />
+
    <path d="M5 5L6 5L6 6L5 6L5 5Z" />
+
    <path d="M6 6L7 6L7 7L6 7L6 6Z" />
+
    <path d="M2 4L3 4L3 8L2 8L2 4Z" />
+
  {:else if name === "comment-cross"}
+
    <path d="M3 3L4 3L4 4L3 4L3 3Z" />
+
    <path d="M12 2L13 2L13 3L12 3L12 2Z" />
+
    <path d="M13 2L14 2L14 3L13 3L13 2Z" />
+
    <path d="M5 11L12 11L12 12L5 12L5 11Z" />
+
    <path d="M3 13L4 13L4 14L3 14L3 13Z" />
+
    <path d="M4 12L5 12L5 13L4 13L4 12Z" />
+
    <path d="M5 11L6 11L6 12L5 12L5 11Z" />
+
    <path d="M2 8L3 8L3 13L2 13L2 8Z" />
+
    <path d="M7 3L8 3L8 4L7 4L7 3Z" />
+
    <path d="M6 2L7 2L7 3L6 3L6 2Z" />
+
    <path d="M8 4L9 4L9 5L8 5L8 4Z" />
+
    <path d="M10 4L11 4L11 5L10 5L10 4Z" />
+
    <path d="M10 6L11 6L11 7L10 7L10 6Z" />
+
    <path d="M11 6L12 6L12 7L11 7L11 6Z" />
+
    <path d="M11 7L12 7L12 8L11 8L11 7Z" />
+
    <path d="M12 7L13 7L13 8L12 8L12 7Z" />
+
    <path d="M12 8L13 8L13 9L12 9L12 8Z" />
+
    <path d="M13 8L14 8L14 9L13 9L13 8Z" />
+
    <path d="M7 8L8 8L8 9L7 9L7 8Z" />
+
    <path d="M6 8L7 8L7 9L6 9L6 8Z" />
+
    <path d="M8 7L9 7L9 8L8 8L8 7Z" />
+
    <path d="M10 6L11 6L11 7L10 7L10 6Z" />
+
    <path d="M9 5L11 5L11 7L9 7L9 5Z" />
+
    <path d="M11 4L12 4L12 5L11 5L11 4Z" />
+
    <path d="M12 3L13 3L13 4L12 4L12 3Z" />
+
    <path d="M11 3L12 3L12 4L11 4L11 3Z" />
+
    <path d="M9 4L10 4L10 5L9 5L9 4Z" />
+
    <path d="M8 6L12 6L12 7L8 7L8 6Z" />
+
    <path d="M8 6L9 6L9 7L8 7L8 6Z" />
+
    <path d="M7 7L8 7L8 8L7 8L7 7Z" />
+
    <path d="M7 2L8 2L8 3L7 3L7 2Z" />
+
    <path d="M8 3L9 3L9 4L8 4L8 3Z" />
+
    <path d="M2 4L3 4L3 8L2 8L2 4Z" />
+
    <path d="M12 10L13 10L13 11L12 11L12 10Z" />
+
    <path d="M6 2L7 2L7 3L6 3L6 2Z" />
+
    <path d="M4 2L5 2L5 3L4 3L4 2Z" />
  {:else if name === "copy"}
    <path d="M6.5 2H13.5V3H6.5V2Z" />
    <path d="M3.5 5H4.5V6H3.5V5Z" />
@@ -387,6 +477,36 @@
    <path d="M7 8.99999H9V9.99998H7V8.99999Z" />
    <path d="M10 6.99998V8.99998L9 8.99999L9 6.99999L10 6.99998Z" />
    <path d="M7 6.99999L7 8.99999L6 8.99998V6.99998L7 6.99999Z" />
+
  {:else if name === "label"}
+
    <path d="M8.5 2.50003H11.5V3.50003H8.5V2.50003Z" />
+
    <path d="M12.5 4.50003H13.5V7.50003H12.5V4.50003Z" />
+
    <path d="M11.5 7.50003H12.5L12.5 8.50003H11.5V7.50003Z" />
+
    <path d="M9.5 9.50003H10.5V10.5H9.5V9.50003Z" />
+
    <path d="M7.5 11.5H8.5V12.5H7.5V11.5Z" />
+
    <path d="M8.5 10.5H9.5V11.5L8.5 11.5V10.5Z" />
+
    <path d="M6.5 12.5H7.5V13.5H6.5V12.5Z" />
+
    <path d="M5.5 11.5H6.5V12.5H5.5V11.5Z" />
+
    <path d="M4.5 10.5H5.5V11.5H4.5V10.5Z" />
+
    <path d="M3.5 9.50003H4.5L4.5 10.5H3.5V9.50003Z" />
+
    <path d="M2.5 8.50003H3.5L3.5 9.50003L2.5 9.50003V8.50003Z" />
+
    <path d="M3.5 7.50003H4.5V8.50003L3.5 8.50003L3.5 7.50003Z" />
+
    <path d="M4.5 6.50003H5.5V7.50003H4.5V6.50003Z" />
+
    <path d="M5.5 5.50003H6.5V6.50003H5.5V5.50003Z" />
+
    <path d="M6.5 4.50003H7.5V5.50003H6.5V4.50003Z" />
+
    <path d="M7.5 3.50003L8.5 3.50003V4.50003L7.5 4.50003V3.50003Z" />
+
    <path d="M9.5 4.50003H10.5V5.50003H9.5V4.50003Z" />
+
    <path d="M9.5 6.50003H10.5V7.50003H9.5V6.50003Z" />
+
    <path d="M8.5 5.50003H9.5L9.5 6.50003H8.5V5.50003Z" />
+
    <path d="M10.5 5.50003L11.5 5.50003V6.50003L10.5 6.50003L10.5 5.50003Z" />
+
    <path d="M11.5 3.50003L12.5 3.50003V4.50003H11.5L11.5 3.50003Z" />
+
    <path d="M12.5 8.50003H13.5V9.50003H12.5V8.50003Z" />
+
    <path d="M9.5 8.50003H10.5L10.5 10.5H9.5L9.5 8.50003Z" />
+
    <path d="M9.5 6.50003H10.5L10.5 8.50003H9.5L9.5 6.50003Z" />
+
    <path d="M12.5 9.50003H13.5V10.5H12.5V9.50003Z" />
+
    <path d="M10.5 10.5H11.5V11.5H10.5L10.5 10.5Z" />
+
    <path d="M12.5 10.5H13.5V11.5H12.5V10.5Z" />
+
    <path d="M10.5 5.50003L11.5 5.50003V6.50003L10.5 6.50003L10.5 5.50003Z" />
+
    <path d="M11.5 11.5H12.5L12.5 12.5H11.5L11.5 11.5Z" />
  {:else if name === "lock"}
    <path d="M6 2H10V3H6V2Z" />
    <path d="M10 3L11 3V4H10V3Z" />
@@ -415,6 +535,16 @@
    <path d="M8 11H9V12H8V11Z" />
    <path d="M7 10H8V12H7V10Z" />
    <path d="M3 4.00003H4V6.00003H3V4.00003Z" />
+
  {:else if name === "merge"}
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M13 11H14V14H11L11 11H12V7H13V11ZM12 13L12 12H13L13 13H12Z" />
+
    <path d="M12 7L9 7L9 9H8L8 8L7 8V7H6V6L7 6V5H8L8 4L9 4V6L12 6L12 7Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M3 5H2L2 2L5 2L5 5H4L4 11H5L5 14H2L2 11H3L3 5ZM4 4V3H3L3 4L4 4ZM3 12H4L4 13H3V12Z" />
  {:else if name === "moon"}
    <path d="M4 3H6V4H4V3Z" />
    <path d="M3 4L4 4L4 6H3V4Z" />
@@ -751,6 +881,27 @@
    <path d="M6 9L7 9L7 10H6V9Z" />
    <path d="M9 9L10 9L10 10H9L9 9Z" />
    <path d="M9 6H10V7H9V6Z" />
+
  {:else if name === "user"}
+
    <path d="M5 3H6V4H5V3Z" />
+
    <path d="M5 6L5 8H4V6H5Z" />
+
    <path d="M12 4V8H11V4H12Z" />
+
    <path d="M4 4L5 4L5 6H4L4 4Z" />
+
    <path d="M10 3H11V4L10 4V3Z" />
+
    <path d="M6 2H10V3L6 3V2Z" />
+
    <path d="M5 8H6V9H5L5 8Z" />
+
    <path d="M6 9H10V10H6V9Z" />
+
    <path d="M10 8L11 8V9L10 9L10 8Z" />
+
    <path d="M9 9H10V10H9V9Z" />
+
    <path d="M10 10H12V11H10L10 10Z" />
+
    <path d="M4 10H6L6 11H4V10Z" />
+
    <path d="M12 11H13V12H12V11Z" />
+
    <path d="M3 11H4L4 12H3V11Z" />
+
    <path d="M13 12H14V13H13V12Z" />
+
    <path d="M2 12H3L3 13H2V12Z" />
+
    <path d="M2 13H14V14H2L2 13Z" />
+
    <path d="M5 3L11 3V5H5V3Z" />
+
    <path d="M9 6H10V7H9V6Z" />
+
    <path d="M6 6H7V7H6V6Z" />
  {:else if name === "warning"}
    <path d="M7 2H9V3H7V2Z" />
    <path d="M6 3H7V5H6V3Z" />
added src/components/PatchTimeline.svelte
@@ -0,0 +1,302 @@
+
<script lang="ts">
+
  import type { Action } from "@bindings/cob/patch/Action";
+
  import type { Operation } from "@bindings/cob/Operation";
+
  import type { Author } from "@bindings/cob/Author";
+

+
  type FlattenedOperation = Action & {
+
    id: string;
+
    author: Author;
+
    timestamp: number;
+
    previous?: Action;
+
  };
+

+
  type StateTracker = Record<Action["type"], Action>;
+

+
  import {
+
    absoluteTimestamp,
+
    authorForNodeId,
+
    formatTimestamp,
+
    patchStatusColor,
+
    publicKeyFromDid,
+
  } from "@app/lib/utils";
+
  import Icon from "./Icon.svelte";
+
  import Id from "./Id.svelte";
+
  import NodeId from "./NodeId.svelte";
+
  import { invoke } from "@app/lib/invoke";
+

+
  interface Props {
+
    patchId: string;
+
    activity: Operation<Action>[];
+
  }
+

+
  /* eslint-disable prefer-const */
+
  let { activity, patchId }: Props = $props();
+
  /* eslint-enable prefer-const */
+

+
  const timeline = $derived(enrichActivity(flattenActivity(activity)));
+

+
  function flattenActivity(
+
    activity: Operation<Action>[],
+
  ): FlattenedOperation[] {
+
    return activity.flatMap(operation =>
+
      operation.actions.map(action => ({
+
        ...action,
+
        id: operation.id,
+
        author: operation.author,
+
        timestamp: operation.timestamp,
+
      })),
+
    );
+
  }
+

+
  function enrichActivity(
+
    flatActivity: FlattenedOperation[],
+
  ): FlattenedOperation[] {
+
    const result: FlattenedOperation[] = [];
+
    const timelineStateTracker: StateTracker = {} as StateTracker;
+

+
    flatActivity.forEach(entry => {
+
      if (timelineStateTracker[entry.type]) {
+
        result.push({ ...entry, previous: timelineStateTracker[entry.type] });
+
      } else {
+
        result.push(entry);
+
      }
+
      timelineStateTracker[entry.type] = entry;
+
    });
+

+
    return result;
+
  }
+

+
  function itemDiff(previousState: string[], newState: string[]) {
+
    const removed = previousState.filter(x => !newState.includes(x));
+
    const added = newState.filter(x => !previousState.includes(x));
+
    return { removed, added };
+
  }
+
</script>
+

+
<style>
+
  .timeline {
+
    display: flex;
+
    gap: 0.75rem;
+
    flex-direction: column;
+
  }
+
  .timeline-item {
+
    display: flex;
+
    align-items: flex-start;
+
    gap: 0.5rem;
+
  }
+
  .wrapper {
+
    display: flex;
+
    flex-wrap: wrap;
+
    gap: 0.25rem;
+
  }
+
  .icon {
+
    padding-top: 3px;
+
  }
+
</style>
+

+
<div class="timeline txt-small">
+
  {#each timeline as op}
+
    {#if op.type === "revision"}
+
      {#if op.id === patchId}
+
        <div class="timeline-item">
+
          <div class="icon" style:color="var(--color-fill-success)">
+
            <Icon name="patch" />
+
          </div>
+
          <div class="wrapper">
+
            <NodeId {...authorForNodeId(op.author)} />
+
            <div>opened patch <Id id={op.id} variant="oid" /></div>
+
            <div title={absoluteTimestamp(op.timestamp)}>
+
              {formatTimestamp(op.timestamp)}
+
            </div>
+
          </div>
+
        </div>
+
      {:else}
+
        <div class="timeline-item">
+
          <div class="icon">
+
            <Icon name="revision" />
+
          </div>
+
          <div class="wrapper">
+
            <NodeId {...authorForNodeId(op.author)} />
+
            <div>created a new revision <Id id={op.id} variant="oid" /></div>
+
            <div title={absoluteTimestamp(op.timestamp)}>
+
              {formatTimestamp(op.timestamp)}
+
            </div>
+
          </div>
+
        </div>
+
      {/if}
+
    {:else if op.type === "lifecycle"}
+
      <div class="timeline-item">
+
        <div class="icon" style:color={patchStatusColor[op.state.status]}>
+
          <Icon name="patch" />
+
        </div>
+
        <div class="wrapper">
+
          <NodeId {...authorForNodeId(op.author)} />
+
          {#if op.state.status === "draft"}
+
            converted patch to draft
+
          {:else if op.state.status === "archived"}
+
            archived patch
+
          {:else if op.state.status === "open"}
+
            reopened patch
+
          {/if}
+
          <div title={absoluteTimestamp(op.timestamp)}>
+
            {formatTimestamp(op.timestamp)}
+
          </div>
+
        </div>
+
      </div>
+
    {:else if op.type === "label" && op.previous && op.previous.type === op.type}
+
      {@const changed = itemDiff(op.previous?.labels ?? [], op.labels)}
+
      {#if changed.added.length || changed.removed.length}
+
        <div class="timeline-item">
+
          <div class="icon">
+
            <Icon name="label" />
+
          </div>
+
          <div class="wrapper">
+
            <NodeId {...authorForNodeId(op.author)} />
+
            {#if changed.added.length}
+
              added label{changed.added.length > 1 ? "s" : ""}
+
              {#each changed.added as label}
+
                <b>{label}</b>
+
              {/each}
+
            {/if}
+
            {#if changed.removed.length}
+
              removed label{changed.removed.length > 1 ? "s" : ""}
+
              {#each changed.removed as label}
+
                <b>{label}</b>
+
              {/each}
+
            {/if}
+
            <div title={absoluteTimestamp(op.timestamp)}>
+
              {formatTimestamp(op.timestamp)}
+
            </div>
+
          </div>
+
        </div>
+
      {/if}
+
    {:else if op.type === "assign" && op.previous && op.previous.type === op.type}
+
      {@const changed = itemDiff(op.previous?.assignees ?? [], op.assignees)}
+
      {#if changed.added.length || changed.removed.length}
+
        <div class="timeline-item">
+
          <div class="icon">
+
            <Icon name="user" />
+
          </div>
+
          <div class="wrapper">
+
            <NodeId {...authorForNodeId(op.author)} />
+
            {#if changed.added.length}
+
              assigned
+
              {#each changed.added as assignee}
+
                {#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
+
                  <NodeId
+
                    {...authorForNodeId({
+
                      did: assignee,
+
                      alias: alias ?? undefined,
+
                    })} />
+
                {/await}
+
              {/each}
+
            {/if}
+
            {#if changed.removed.length}
+
              unassigned
+
              {#each changed.removed as assignee}
+
                {#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
+
                  <NodeId
+
                    {...authorForNodeId({
+
                      did: assignee,
+
                      alias: alias ?? undefined,
+
                    })} />
+
                {/await}
+
              {/each}
+
            {/if}
+
            <div title={absoluteTimestamp(op.timestamp)}>
+
              {formatTimestamp(op.timestamp)}
+
            </div>
+
          </div>
+
        </div>
+
      {/if}
+
    {:else if op.type === "merge"}
+
      <div class="timeline-item">
+
        <div class="icon" style:color="var(--color-fill-primary)">
+
          <Icon name="patch" />
+
        </div>
+
        <div class="wrapper">
+
          <NodeId {...authorForNodeId(op.author)} />
+
          <div>
+
            merged patch at revision <Id id={op.revision} variant="oid" />
+
          </div>
+
          <div title={absoluteTimestamp(op.timestamp)}>
+
            {formatTimestamp(op.timestamp)}
+
          </div>
+
        </div>
+
      </div>
+
    {:else if op.type === "edit"}
+
      {#if op.previous && op.previous.type === op.type}
+
        <div class="timeline-item">
+
          <div class="icon">
+
            <Icon name="pen" />
+
          </div>
+
          <div class="wrapper">
+
            <NodeId {...authorForNodeId(op.author)} />
+
            changed title
+
            <s>{op.previous.title}</s>
+
            -> {op.title}
+
            <div title={absoluteTimestamp(op.timestamp)}>
+
              {formatTimestamp(op.timestamp)}
+
            </div>
+
          </div>
+
        </div>
+
      {/if}
+
    {:else if op.type === "review"}
+
      <div class="timeline-item">
+
        {#if op.verdict === "accept"}
+
          <div class="icon" style:color="var(--color-foreground-success)">
+
            <Icon name="comment-checkmark" />
+
          </div>
+
          <div class="wrapper">
+
            <NodeId {...authorForNodeId(op.author)} />
+
            accepted revision <Id id={op.revision} variant="oid" />
+
            <div title={absoluteTimestamp(op.timestamp)}>
+
              {formatTimestamp(op.timestamp)}
+
            </div>
+
          </div>
+
        {:else}
+
          <div class="icon" style:color="var(--color-foreground-red)">
+
            <Icon name="comment-cross" />
+
          </div>
+
          <div class="wrapper">
+
            <NodeId {...authorForNodeId(op.author)} />
+
            rejected revision <Id id={op.revision} variant="oid" />
+
            <div title={absoluteTimestamp(op.timestamp)}>
+
              {formatTimestamp(op.timestamp)}
+
            </div>
+
          </div>
+
        {/if}
+
      </div>
+
    {:else if op.type === "review.comment"}
+
      <div class="timeline-item">
+
        <div class="icon">
+
          <Icon name="comment" />
+
        </div>
+
        <div class="wrapper">
+
          <NodeId {...authorForNodeId(op.author)} />
+
          {op.reply_to ? "replied to a comment" : "commented"} on review <Id
+
            id={op.review}
+
            variant="oid" />
+
          <div title={absoluteTimestamp(op.timestamp)}>
+
            {formatTimestamp(op.timestamp)}
+
          </div>
+
        </div>
+
      </div>
+
    {:else if op.type === "revision.comment"}
+
      <div class="timeline-item">
+
        <div class="icon">
+
          <Icon name="comment" />
+
        </div>
+
        <div class="wrapper">
+
          <NodeId {...authorForNodeId(op.author)} />
+
          {op.replyTo ? "replied to a comment" : "commented"} on revision <Id
+
            id={op.revision}
+
            variant="oid" />
+
          <div title={absoluteTimestamp(op.timestamp)}>
+
            {formatTimestamp(op.timestamp)}
+
          </div>
+
        </div>
+
      </div>
+
    {/if}
+
  {/each}
+
</div>
deleted src/components/PatchTimelineLifecycleAction.svelte
@@ -1,28 +0,0 @@
-
<script lang="ts">
-
  import type { Operation } from "@bindings/cob/Operation";
-
  import type { Action } from "@bindings/cob/patch/Action";
-

-
  import { authorForNodeId, formatTimestamp } from "@app/lib/utils";
-

-
  import Border from "@app/components/Border.svelte";
-
  import NodeId from "@app/components/NodeId.svelte";
-
  import PatchStateBadge from "./PatchStateBadge.svelte";
-

-
  interface Props {
-
    action: Extract<Action, { type: "lifecycle" }>;
-
    op: Operation<Action>;
-
  }
-

-
  const { op, action }: Props = $props();
-
</script>
-

-
<Border variant="float" stylePadding="1rem">
-
  <div class="txt-small">
-
    <div class="global-flex txt-small">
-
      <NodeId {...authorForNodeId(op.author)} />
-
      changed status to
-
      <PatchStateBadge state={action.state} />
-
      {formatTimestamp(op.timestamp)}
-
    </div>
-
  </div>
-
</Border>
modified src/views/repo/Patch.svelte
@@ -1,10 +1,10 @@
<script lang="ts">
-
  import type { Operation } from "@bindings/cob/Operation";
  import type { Action } from "@bindings/cob/patch/Action";
  import type { Author } from "@bindings/cob/Author";
  import type { Config } from "@bindings/config/Config";
  import type { Diff } from "@bindings/diff/Diff";
  import type { Embed } from "@bindings/cob/thread/Embed";
+
  import type { Operation } from "@bindings/cob/Operation";
  import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
  import type { Patch } from "@bindings/cob/patch/Patch";
  import type { PatchStatus } from "./router";
@@ -38,6 +38,7 @@
  import PatchStateBadge from "@app/components/PatchStateBadge.svelte";
  import PatchStateButton from "@app/components/PatchStateButton.svelte";
  import PatchTeaser from "@app/components/PatchTeaser.svelte";
+
  import PatchTimeline from "@app/components/PatchTimeline.svelte";
  import Sidebar from "@app/components/Sidebar.svelte";
  import TextInput from "@app/components/TextInput.svelte";

@@ -73,6 +74,8 @@
  let assigneesSaveInProgress: boolean = $state(false);
  let tab: "patch" | "revisions" = $state("patch");

+
  let hideTimeline = $state(false);
+

  $effect(() => {
    items = patches.content;
    cursor = patches.cursor;
@@ -86,6 +89,7 @@
    tab = "patch";
    editingTitle = false;
    updatedTitle = patch.title;
+
    hideTimeline = false;
  });

  const project = $derived(repo.payloads["xyz.radicle.project"]!);
@@ -111,6 +115,10 @@
      rid: rid,
      id: patchId,
    });
+
    activity = await invoke<Operation<Action>[]>("activity_by_patch", {
+
      rid: repo.rid,
+
      id: patch.id,
+
    });
  }

  async function loadMoreSecondColumn() {
@@ -168,7 +176,7 @@
  }

  async function reload() {
-
    [config, repo, patches, patch, revisions] = await Promise.all([
+
    [config, repo, patches, patch, revisions, activity] = await Promise.all([
      invoke<Config>("config"),
      invoke<RepoInfo>("repo_by_id", {
        rid: repo.rid,
@@ -185,6 +193,10 @@
        rid: repo.rid,
        id: patch.id,
      }),
+
      invoke<Operation<Action>[]>("activity_by_patch", {
+
        rid: repo.rid,
+
        id: patch.id,
+
      }),
    ]);
  }

@@ -323,7 +335,7 @@
  }

  .patch-body {
-
    margin-top: 1rem;
+
    margin: 1rem 0;
    position: relative;
  }
  /* We put the background and clip-path in a separate element to prevent
@@ -357,6 +369,9 @@
    margin-bottom: 0.5rem;
    color: var(--color-foreground-dim);
  }
+
  .hide-timeline {
+
    display: none;
+
  }
</style>

<Layout {loadMoreSecondColumn} publicKey={config.publicKey}>
@@ -539,16 +554,22 @@
          ) && partial(editRevision, revisions[0].id)}>
        </CommentComponent>
      </div>
-
      <div class="connector"></div>
+

      <div>
-
        {#each activity as op}
-
          {#each op.actions as action}
-
            {#if action.type === "revision"}
-
              <div>New revision created {action.oid}</div>
-
              <div class="connector"></div>
-
            {/if}
-
          {/each}
-
        {/each}
+
        <!-- 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={() => (hideTimeline = !hideTimeline)}>
+
          <Icon
+
            name={hideTimeline ? "chevron-right" : "chevron-down"} />Timeline
+
        </div>
+
        <div class:hide-timeline={hideTimeline}>
+
          <PatchTimeline {activity} patchId={patch.id} />
+
        </div>
      </div>
    {:else}
      {@const revision = revisions.slice(-1)[0]}