Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add track button to project header
Rūdolfs Ošiņš committed 2 years ago
commit cdf62f2883bfd78c666c7de1588eeb5ebc4e3d78
parent 94c15a27191e6851bd5d68fcd84b45abfe5ec445
13 files changed +172 -36
modified src/components/IconSmall.svelte
@@ -33,6 +33,8 @@
    | "network"
    | "patch"
    | "plus"
+
    | "tracking-on"
+
    | "tracking-off"
    | "user";
</script>

@@ -375,6 +377,30 @@
      fill-rule="evenodd"
      clip-rule="evenodd"
      d="M13.1667 8C13.1667 8.27614 12.9428 8.5 12.6667 8.5L3.33334 8.5C3.0572 8.5 2.83334 8.27614 2.83334 8C2.83334 7.72386 3.0572 7.5 3.33334 7.5L12.6667 7.5C12.9428 7.5 13.1667 7.72386 13.1667 8Z" />
+
  {:else if name === "tracking-on"}
+
    <path
+
      d="M1 1H4.26667C4.5244 1 4.73333 1.20893 4.73333 1.46667C4.73333 1.7244 4.5244 1.93333 4.26667 1.93333H1.93333V4.26667C1.93333 4.5244 1.7244 4.73333 1.46667 4.73333C1.20893 4.73333 1 4.5244 1 4.26667V1Z" />
+
    <path
+
      d="M15 1V4.26667C15 4.5244 14.7911 4.73333 14.5333 4.73333C14.2756 4.73333 14.0667 4.5244 14.0667 4.26667V1.93333L11.7333 1.93333C11.4756 1.93333 11.2667 1.7244 11.2667 1.46667C11.2667 1.20893 11.4756 1 11.7333 1H15Z" />
+
    <path
+
      d="M11.7333 15H15V11.7333C15 11.4756 14.7911 11.2667 14.5333 11.2667C14.2756 11.2667 14.0667 11.4756 14.0667 11.7333V14.0667H11.7333C11.4756 14.0667 11.2667 14.2756 11.2667 14.5333C11.2667 14.7911 11.4756 15 11.7333 15Z" />
+
    <path
+
      d="M1 15V11.7333C1 11.4756 1.20893 11.2667 1.46667 11.2667C1.7244 11.2667 1.93333 11.4756 1.93333 11.7333L1.93333 14.0667H4.26667C4.5244 14.0667 4.73333 14.2756 4.73333 14.5333C4.73333 14.7911 4.5244 15 4.26667 15H1Z" />
+
    <path
+
      d="M9.86667 8C9.86667 9.03093 9.03093 9.86667 8 9.86667C6.96907 9.86667 6.13333 9.03093 6.13333 8C6.13333 6.96907 6.96907 6.13333 8 6.13333C9.03093 6.13333 9.86667 6.96907 9.86667 8Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M8.46667 2.41917C11.1872 2.64361 13.3564 4.81276 13.5808 7.53333H14.5333C14.7911 7.53333 15 7.74227 15 8C15 8.25773 14.7911 8.46667 14.5333 8.46667H13.5808C13.3564 11.1872 11.1872 13.3564 8.46667 13.5808V14.5333C8.46667 14.7911 8.25773 15 8 15C7.74227 15 7.53333 14.7911 7.53333 14.5333V13.5808C4.81276 13.3564 2.64361 11.1872 2.41917 8.46667H1.46667C1.20893 8.46667 1 8.25773 1 8C1 7.74227 1.20893 7.53333 1.46667 7.53333H2.41917C2.64361 4.81276 4.81276 2.64361 7.53333 2.41917V1.46667C7.53333 1.20893 7.74227 1 8 1C8.25773 1 8.46667 1.20893 8.46667 1.46667V2.41917ZM12.6437 8.4661C12.425 10.6713 10.6713 12.425 8.4661 12.6437C8.45412 12.3966 8.25003 12.2 8 12.2C7.74997 12.2 7.54588 12.3966 7.5339 12.6437C5.32872 12.425 3.575 10.6713 3.35632 8.46611C3.60338 8.45413 3.8 8.25003 3.8 8C3.8 7.74997 3.60338 7.54587 3.35632 7.53389C3.575 5.32872 5.32872 3.575 7.53389 3.35632C7.54587 3.60338 7.74997 3.8 8 3.8C8.25003 3.8 8.45413 3.60338 8.46611 3.35632C10.6713 3.575 12.425 5.32872 12.6437 7.5339C12.3966 7.54588 12.2 7.74997 12.2 8C12.2 8.25003 12.3966 8.45412 12.6437 8.4661Z" />
+
  {:else if name === "tracking-off"}
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M11.7333 8C11.7333 10.0619 10.0619 11.7333 8 11.7333C5.93814 11.7333 4.26667 10.0619 4.26667 8C4.26667 5.93814 5.93814 4.26667 8 4.26667C10.0619 4.26667 11.7333 5.93814 11.7333 8ZM10.8 8C10.8 9.5464 9.5464 10.8 8 10.8C6.4536 10.8 5.2 9.5464 5.2 8C5.2 6.4536 6.4536 5.2 8 5.2C9.5464 5.2 10.8 6.4536 10.8 8Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M8.46667 2.41917V1.46667C8.46667 1.20893 8.25773 1 8 1C7.74227 1 7.53333 1.20893 7.53333 1.46667V2.41917C4.81276 2.64361 2.64361 4.81276 2.41917 7.53333H1.46667C1.20893 7.53333 1 7.74227 1 8C1 8.25773 1.20893 8.46667 1.46667 8.46667H2.41917C2.64361 11.1872 4.81276 13.3564 7.53333 13.5808V14.5333C7.53333 14.7911 7.74227 15 8 15C8.25773 15 8.46667 14.7911 8.46667 14.5333V13.5808C11.1872 13.3564 13.3564 11.1872 13.5808 8.46667H14.5333C14.7911 8.46667 15 8.25773 15 8C15 7.74227 14.7911 7.53333 14.5333 7.53333H13.5808C13.3564 4.81276 11.1872 2.64361 8.46667 2.41917ZM7.53389 3.35632C7.54587 3.60338 7.74997 3.8 8 3.8C8.25003 3.8 8.45413 3.60338 8.46611 3.35632C10.6713 3.575 12.425 5.32872 12.6437 7.5339C12.3966 7.54588 12.2 7.74997 12.2 8C12.2 8.25003 12.3966 8.45412 12.6437 8.4661C12.425 10.6713 10.6713 12.425 8.4661 12.6437C8.45412 12.3966 8.25003 12.2 8 12.2C7.74997 12.2 7.54588 12.3966 7.5339 12.6437C5.32872 12.425 3.575 10.6713 3.35632 8.46611C3.60338 8.45413 3.8 8.25003 3.8 8C3.8 7.74997 3.60338 7.54587 3.35632 7.53389C3.575 5.32871 5.32871 3.575 7.53389 3.35632Z" />
  {:else if name === "user"}
    <path
      d="M12.8336 5.28766C12.1895 5.93182 3.99733 12.0026 3.99733 12.0026C3.99733 12.0026 10.0681 3.8105 10.7123 3.16634C11.3564 2.52219 12.3535 2.47487 12.9393 3.06066C13.5251 3.64645 13.4778 4.64351 12.8336 5.28766Z" />
modified src/views/projects/Commit.svelte
@@ -12,6 +12,7 @@
  export let baseUrl: BaseUrl;
  export let commit: Commit;
  export let project: Project;
+
  export let tracking: boolean;

  $: header = commit.commit;
</script>
@@ -41,7 +42,7 @@
  }
</style>

-
<Layout {baseUrl} {project}>
+
<Layout {baseUrl} {project} {tracking}>
  <div class="header">
    <div class="summary">
      <div class="txt-medium txt-bold">
modified src/views/projects/Header.svelte
@@ -71,21 +71,5 @@
        </div>
      </Button>
    </Link>
-

-
    <div class="layout-desktop">
-
      <Button
-
        disabled
-
        variant="background"
-
        title="Tracked by {project.trackings} {pluralize(
-
          'node',
-
          project.trackings,
-
        )}">
-
        <IconSmall name="network" />
-
        <div>
-
          {project.trackings}
-
          {pluralize("node", project.trackings)}
-
        </div>
-
      </Button>
-
    </div>
  </Radio>
</div>
added src/views/projects/Header/TrackButton.svelte
@@ -0,0 +1,67 @@
+
<script lang="ts">
+
  import { pluralize } from "@app/lib/pluralize";
+

+
  import Button from "@app/components/Button.svelte";
+
  import Command from "@app/components/Command.svelte";
+
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import Popover from "@app/components/Popover.svelte";
+

+
  export let projectId: string;
+
  export let trackings: number;
+
  export let tracking: boolean;
+

+
  $: buttonTitle = tracking ? "Tracking" : "Track";
+
  $: command = tracking ? "untrack" : "track";
+
</script>
+

+
<style>
+
  .track-label {
+
    display: block;
+
    font-size: var(--font-size-small);
+
    font-weight: var(--font-weight-regular);
+
    margin-bottom: 0.75rem;
+
  }
+
</style>
+

+
<Popover
+
  popoverPositionTop="3rem"
+
  popoverPositionRight="0"
+
  popoverWidth="26rem">
+
  <Button
+
    slot="toggle"
+
    size="large"
+
    variant="outline"
+
    title="Tracked by {trackings} {pluralize('node', trackings)}">
+
    {#if tracking}
+
      <IconSmall name="tracking-on" />
+
    {:else}
+
      <IconSmall name="tracking-off" />
+
    {/if}
+
    <span>
+
      {buttonTitle}
+
      <span
+
        style:color="var(--color-foreground-dim)"
+
        style:font-weight="var(--font-weight-regular)">
+
        {trackings}
+
      </span>
+
    </span>
+
  </Button>
+

+
  <svelte:fragment slot="popover">
+
    <div class="track-label">
+
      Use the <a
+
        target="_blank"
+
        rel="noreferrer"
+
        href="https://radicle.xyz/#try"
+
        class="txt-link txt-bold">
+
        Radicle CLI
+
      </a>
+
      to {command} this project.
+
      <br />
+
      <br />
+
      Tracking means subscribing to updates from this repository. It also helps with
+
      increasing its availability over the Radicle network.
+
    </div>
+
    <Command command={`rad ${command} ${projectId}`} />
+
  </svelte:fragment>
+
</Popover>
modified src/views/projects/History.svelte
@@ -29,6 +29,7 @@
  export let revision: string | undefined;
  export let totalCommitCount: number;
  export let tree: Tree;
+
  export let tracking: boolean;

  const api = new HttpdClient(baseUrl);

@@ -114,7 +115,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="source">
+
<Layout {baseUrl} {project} {tracking} activeTab="source">
  <svelte:fragment slot="subheader">
    <div style:margin-top="1rem">
      <Header
modified src/views/projects/Issue.svelte
@@ -32,6 +32,7 @@
  export let baseUrl: BaseUrl;
  export let issue: Issue;
  export let project: Project;
+
  export let tracking: boolean;

  const rawPath = utils.getRawBasePath(project.id, baseUrl, project.head);
  const api = new HttpdClient(baseUrl);
@@ -371,7 +372,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="issues">
+
<Layout {baseUrl} {project} {tracking} activeTab="issues">
  <div class="issue">
    <div style="display: flex; flex-direction: column; gap: 1.5rem;">
      <CobHeader
modified src/views/projects/Issue/New.svelte
@@ -23,6 +23,7 @@

  export let baseUrl: BaseUrl;
  export let project: Project;
+
  export let tracking: boolean;

  let newEmbeds: Embed[] = [];
  let selectionStart = 0;
@@ -148,7 +149,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="issues">
+
<Layout {baseUrl} {project} {tracking} activeTab="issues">
  <main>
    {#if $httpdStore.state === "authenticated"}
      {@const session = $httpdStore.session}
modified src/views/projects/Issues.svelte
@@ -25,6 +25,7 @@
  export let issues: Issue[];
  export let project: Project;
  export let state: IssueState["status"];
+
  export let tracking: boolean;

  let loading = false;
  let page = 0;
@@ -78,7 +79,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="issues">
+
<Layout {baseUrl} {project} {tracking} activeTab="issues">
  <div class="issues">
    <List items={allIssues}>
      <div slot="header" style="display: flex;">
modified src/views/projects/Layout.svelte
@@ -7,16 +7,18 @@
  import markdown from "@app/lib/markdown";
  import { twemoji, isLocal } from "@app/lib/utils";

+
  import Button from "@app/components/Button.svelte";
  import Clipboard from "@app/components/Clipboard.svelte";
  import CloneButton from "@app/views/projects/Header/CloneButton.svelte";
  import Link from "@app/components/Link.svelte";
-
  import Button from "@app/components/Button.svelte";

  import Header from "./Header.svelte";
+
  import TrackButton from "./Header/TrackButton.svelte";

  export let activeTab: ActiveTab = undefined;
  export let baseUrl: BaseUrl;
  export let project: Project;
+
  export let tracking: boolean;

  const render = (content: string): string =>
    dompurify.sanitize(markdown.parse(content) as string);
@@ -117,7 +119,10 @@
          {isLocal(baseUrl.hostname) ? "radicle.local" : baseUrl.hostname}
        </Button>
      </Link>
-

+
      <TrackButton
+
        {tracking}
+
        trackings={project.trackings}
+
        projectId={project.id} />
      <CloneButton {baseUrl} id={project.id} name={project.name} />
    </div>
  </div>
modified src/views/projects/Patch.svelte
@@ -74,6 +74,7 @@
  export let patch: Patch;
  export let project: Project;
  export let view: PatchView;
+
  export let tracking: boolean;

  $: api = new HttpdClient(baseUrl);

@@ -440,7 +441,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="patches">
+
<Layout {baseUrl} {project} {tracking} activeTab="patches">
  <div class="patch">
    <div>
      <CobHeader id={patch.id} title={patch.title}>
modified src/views/projects/Patches.svelte
@@ -22,6 +22,7 @@
  export let patches: Patch[];
  export let project: Project;
  export let state: PatchState["status"];
+
  export let tracking: boolean;

  let loading = false;
  let page = 0;
@@ -83,7 +84,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="patches">
+
<Layout {baseUrl} {project} {tracking} activeTab="patches">
  <div class="patches">
    <List items={allPatches}>
      <div slot="header" style="display: flex;">
modified src/views/projects/Source.svelte
@@ -25,6 +25,7 @@
  export let project: Project;
  export let revision: string | undefined;
  export let tree: Tree;
+
  export let tracking: boolean;

  // Whether the mobile file tree is visible.
  let mobileFileTree = false;
@@ -125,7 +126,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="source">
+
<Layout {baseUrl} {project} {tracking} activeTab="source">
  <svelte:fragment slot="subheader">
    <div style:margin-top="1rem">
      <Header
modified src/views/projects/router.ts
@@ -21,8 +21,9 @@ import type {

import { HttpdClient } from "@httpd-client";
import * as Syntax from "@app/lib/syntax";
-
import { unreachable } from "@app/lib/utils";
+
import { isLocal, unreachable } from "@app/lib/utils";
import { nodePath } from "@app/views/nodes/router";
+
import * as httpd from "@app/lib/httpd";

export const COMMITS_PER_PAGE = 30;
export const PATCHES_PER_PAGE = 10;
@@ -113,6 +114,7 @@ export type ProjectLoadedRoute =
        tree: Tree;
        path: string;
        blobResult: BlobResult;
+
        tracking: boolean;
      };
    }
  | {
@@ -127,6 +129,7 @@ export type ProjectLoadedRoute =
        tree: Tree;
        commitHeaders: CommitHeader[];
        totalCommitCount: number;
+
        tracking: boolean;
      };
    }
  | {
@@ -135,6 +138,7 @@ export type ProjectLoadedRoute =
        baseUrl: BaseUrl;
        project: Project;
        commit: Commit;
+
        tracking: boolean;
      };
    }
  | {
@@ -142,8 +146,8 @@ export type ProjectLoadedRoute =
      params: {
        baseUrl: BaseUrl;
        project: Project;
-

        issue: Issue;
+
        tracking: boolean;
      };
    }
  | {
@@ -153,6 +157,7 @@ export type ProjectLoadedRoute =
        project: Project;
        issues: Issue[];
        state: IssueState["status"];
+
        tracking: boolean;
      };
    }
  | {
@@ -160,6 +165,7 @@ export type ProjectLoadedRoute =
      params: {
        baseUrl: BaseUrl;
        project: Project;
+
        tracking: boolean;
      };
    }
  | {
@@ -169,6 +175,7 @@ export type ProjectLoadedRoute =
        project: Project;
        patches: Patch[];
        state: PatchState["status"];
+
        tracking: boolean;
      };
    }
  | {
@@ -178,6 +185,7 @@ export type ProjectLoadedRoute =
        project: Project;
        patch: Patch;
        view: PatchView;
+
        tracking: boolean;
      };
    };

@@ -232,19 +240,40 @@ function parseRevisionToOid(
  }
}

+
async function isLocalNodeTracking(route: ProjectRoute): Promise<boolean> {
+
  if (isLocal(route.node.hostname)) {
+
    return true;
+
  } else {
+
    try {
+
      await httpd.api.project.getById(route.project);
+
      return true;
+
    } catch (error: any) {
+
      if (error.status === 404) {
+
        return false;
+
      } else {
+
        // Either `radicle-httpd` isn't running or there was some other
+
        // error.
+
        return false;
+
      }
+
    }
+
  }
+
}
+

export async function loadProjectRoute(
  route: ProjectRoute,
): Promise<ProjectLoadedRoute | LoadErrorRoute | NotFoundRoute> {
  const api = new HttpdClient(route.node);
+

  try {
    if (route.resource === "project.source") {
      return await loadTreeView(route);
    } else if (route.resource === "project.history") {
      return await loadHistoryView(route);
    } else if (route.resource === "project.commit") {
-
      const [project, commit] = await Promise.all([
+
      const [project, commit, tracking] = await Promise.all([
        api.project.getById(route.project),
        api.project.getCommitBySha(route.project, route.commit),
+
        isLocalNodeTracking(route),
      ]);

      return {
@@ -253,12 +282,14 @@ export async function loadProjectRoute(
          baseUrl: route.node,
          project,
          commit,
+
          tracking,
        },
      };
    } else if (route.resource === "project.issue") {
-
      const [project, issue] = await Promise.all([
+
      const [project, issue, tracking] = await Promise.all([
        api.project.getById(route.project),
        api.project.getIssueById(route.project, route.issue),
+
        isLocalNodeTracking(route),
      ]);
      return {
        resource: "project.issue",
@@ -266,6 +297,7 @@ export async function loadProjectRoute(
          baseUrl: route.node,
          project,
          issue,
+
          tracking,
        },
      };
    } else if (route.resource === "project.patch") {
@@ -273,12 +305,16 @@ export async function loadProjectRoute(
    } else if (route.resource === "project.issues") {
      return await loadIssuesView(route);
    } else if (route.resource === "project.newIssue") {
-
      const project = await api.project.getById(route.project);
+
      const [project, tracking] = await Promise.all([
+
        api.project.getById(route.project),
+
        isLocalNodeTracking(route),
+
      ]);
      return {
        resource: "project.newIssue",
        params: {
          baseUrl: route.node,
          project,
+
          tracking,
        },
      };
    } else if (route.resource === "project.patches") {
@@ -326,13 +362,14 @@ async function loadPatchesView(
  const searchParams = new URLSearchParams(route.search || "");
  const state = (searchParams.get("state") as PatchState["status"]) || "open";

-
  const [project, patches] = await Promise.all([
+
  const [project, patches, tracking] = await Promise.all([
    api.project.getById(route.project),
    api.project.getAllPatches(route.project, {
      state,
      page: 0,
      perPage: PATCHES_PER_PAGE,
    }),
+
    isLocalNodeTracking(route),
  ]);

  return {
@@ -342,6 +379,7 @@ async function loadPatchesView(
      patches,
      state,
      project,
+
      tracking,
    },
  };
}
@@ -352,13 +390,14 @@ async function loadIssuesView(
  const api = new HttpdClient(route.node);
  const state = route.state || "open";

-
  const [project, issues] = await Promise.all([
+
  const [project, issues, tracking] = await Promise.all([
    api.project.getById(route.project),
    api.project.getAllIssues(route.project, {
      state,
      page: 0,
      perPage: ISSUES_PER_PAGE,
    }),
+
    isLocalNodeTracking(route),
  ]);

  return {
@@ -368,6 +407,7 @@ async function loadIssuesView(
      issues,
      state,
      project,
+
      tracking,
    },
  };
}
@@ -377,10 +417,11 @@ async function loadTreeView(
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);

-
  const [project, peers, branchMap] = await Promise.all([
+
  const [project, peers, branchMap, tracking] = await Promise.all([
    api.project.getById(route.project),
    api.project.getAllRemotes(route.project),
    getPeerBranches(api, route.project, route.peer),
+
    isLocalNodeTracking(route),
  ]);

  if (route.route) {
@@ -416,6 +457,7 @@ async function loadTreeView(
      tree,
      path,
      blobResult,
+
      tracking,
    },
  };
}
@@ -486,13 +528,14 @@ async function loadHistoryView(
    );
  }

-
  const [tree, commitsResponse] = await Promise.all([
+
  const [tree, commitsResponse, tracking] = await Promise.all([
    api.project.getTree(route.project, commitId),
    await api.project.getAllCommits(project.id, {
      parent: commitId,
      page: 0,
      perPage: COMMITS_PER_PAGE,
    }),
+
    isLocalNodeTracking(route),
  ]);

  return {
@@ -507,6 +550,7 @@ async function loadHistoryView(
      tree,
      commitHeaders: commitsResponse.commits.map(c => c.commit),
      totalCommitCount: commitsResponse.stats.commits,
+
      tracking,
    },
  };
}
@@ -515,9 +559,10 @@ async function loadPatchView(
  route: ProjectPatchRoute,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
-
  const [project, patch] = await Promise.all([
+
  const [project, patch, tracking] = await Promise.all([
    api.project.getById(route.project),
    api.project.getPatchById(route.project, route.patch),
+
    isLocalNodeTracking(route),
  ]);
  const latestRevision = patch.revisions[patch.revisions.length - 1];

@@ -572,6 +617,7 @@ async function loadPatchView(
      project,
      patch,
      view,
+
      tracking,
    },
  };
}