Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add delegates and node scope policies to Sidebar
Sebastian Martinez committed 1 year ago
commit 6695660a17d651be970e0ff472c93d4f3141b524
parent 29d340961b9df7ad6fd11fead1ff804e4cd28c7b
16 files changed +458 -137
modified src/components/IconSmall.svelte
@@ -53,6 +53,7 @@
    | "online"
    | "patch"
    | "plus"
+
    | "quorum"
    | "repo"
    | "seedling"
    | "settings"
@@ -499,6 +500,43 @@
      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 === "quorum"}
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M12 4C12.5523 4 13 3.55228 13 3C13 2.44772 12.5523 2 12 2C11.4477 2 11 2.44772 11 3C11 3.55228 11.4477 4 12 4ZM12 5C13.1046 5 14 4.10457 14 3C14 1.89543 13.1046 1 12 1C10.8954 1 10 1.89543 10 3C10 4.10457 10.8954 5 12 5Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M4 14C4.55228 14 5 13.5523 5 13C5 12.4477 4.55228 12 4 12C3.44772 12 3 12.4477 3 13C3 13.5523 3.44772 14 4 14ZM4 15C5.10457 15 6 14.1046 6 13C6 11.8954 5.10457 11 4 11C2.89543 11 2 11.8954 2 13C2 14.1046 2.89543 15 4 15Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M4 4C4.55228 4 5 3.55228 5 3C5 2.44772 4.55228 2 4 2C3.44772 2 3 2.44772 3 3C3 3.55228 3.44772 4 4 4ZM4 5C5.10457 5 6 4.10457 6 3C6 1.89543 5.10457 1 4 1C2.89543 1 2 1.89543 2 3C2 4.10457 2.89543 5 4 5Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M12 14C12.5523 14 13 13.5523 13 13C13 12.4477 12.5523 12 12 12C11.4477 12 11 12.4477 11 13C11 13.5523 11.4477 14 12 14ZM12 15C13.1046 15 14 14.1046 14 13C14 11.8954 13.1046 11 12 11C10.8954 11 10 11.8954 10 13C10 14.1046 10.8954 15 12 15Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M8 3.5C8.15738 3.5 8.30557 3.5741 8.4 3.7L11.4 7.7C11.5333 7.87778 11.5333 8.12222 11.4 8.3L8.4 12.3C8.30557 12.4259 8.15738 12.5 8 12.5C7.84262 12.5 7.69443 12.4259 7.6 12.3L4.6 8.3C4.46667 8.12222 4.46667 7.87778 4.6 7.7L7.6 3.7C7.69443 3.5741 7.84262 3.5 8 3.5ZM5.625 8L8 11.1667L10.375 8L8 4.83333L5.625 8Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M4.49592 11.5631L6.0103 9.64167L6.79568 10.2607L5.2813 12.1821L4.49592 11.5631Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M9.09293 5.6512L10.6073 3.7298L11.3927 4.34882L9.87831 6.27021L9.09293 5.6512Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M11.3927 11.5631L9.87831 9.64167L9.09293 10.2607L10.6073 12.1821L11.3927 11.5631Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M6.79568 5.6512L5.28129 3.7298L4.49591 4.34882L6.0103 6.27021L6.79568 5.6512Z" />
  {:else if name === "repo"}
    <path
      fill-rule="evenodd"
modified src/components/ScopePolicyExplainer.svelte
@@ -1,17 +1,41 @@
<script lang="ts">
  import type { Scope, Policy } from "@httpd-client";

+
  import { capitalize } from "lodash";
+

  export let scope: Scope;
  export let policy: Policy;
</script>

-
{#if policy === "allow"}
-
  All discovered repositories will get seeded,
-
{:else if policy === "block"}
-
  Only repositories marked as such will get seeded,
-
{/if}
-
{#if scope === "all"}
-
  and all changes in those repositories, made by any peer, will be synced.
-
{:else if scope === "followed"}
-
  and only changes made by explicitly followed peers will be synced.
-
{/if}
+
<style>
+
  .section {
+
    display: flex;
+
    justify-content: space-between;
+
    align-items: center;
+
    padding: 0.5rem 0;
+
  }
+
</style>
+

+
<div class="section">
+
  Scope:
+
  <span class="txt-bold">{capitalize(scope)}</span>
+
</div>
+
<div class="txt-missing">
+
  {#if scope === "all"}
+
    All changes in seeded repositories, made by any peer, will be synced.
+
  {:else if scope === "followed"}
+
    Only changes made by explicitly followed peers will be synced.
+
  {/if}
+
</div>
+

+
<div class="section">
+
  Policy:
+
  <span class="txt-bold">{capitalize(policy)}</span>
+
</div>
+
<div class="txt-missing">
+
  {#if policy === "allow"}
+
    All discovered repositories will get seeded.
+
  {:else if policy === "block"}
+
    Only repositories marked as such will get seeded.
+
  {/if}
+
</div>
modified src/views/nodes/View.svelte
@@ -1,6 +1,8 @@
<script lang="ts">
  import type { BaseUrl, NodeStats, Policy, Scope } from "@httpd-client";

+
  import { capitalize } from "lodash";
+

  import * as router from "@app/lib/router";
  import { api, httpdStore } from "@app/lib/httpd";
  import { baseUrlToString, isLocal, truncateId } from "@app/lib/utils";
@@ -10,9 +12,12 @@

  import AppLayout from "@app/App/AppLayout.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
+
  import IconButton from "@app/components/IconButton.svelte";
+
  import IconSmall from "@app/components/IconSmall.svelte";
  import Loading from "@app/components/Loading.svelte";
+
  import Popover from "@app/components/Popover.svelte";
  import ProjectCard from "@app/components/ProjectCard.svelte";
-
  import ScopePolicyPopover from "@app/views/nodes/ScopePolicyPopover.svelte";
+
  import ScopePolicyExplainer from "@app/components/ScopePolicyExplainer.svelte";

  export let baseUrl: BaseUrl;
  export let nid: string;
@@ -22,6 +27,8 @@
  export let policy: Policy | undefined = undefined;
  export let scope: Scope | undefined = undefined;

+
  $: shortScope =
+
    scope === "all" && policy === "allow" ? "permissive" : "restrictive";
  $: hostname = isLocal(baseUrl.hostname) ? "Local Node" : baseUrl.hostname;
  $: session =
    $httpdStore.state === "authenticated" && isLocal(api.baseUrl.hostname)
@@ -68,6 +75,11 @@
    font-family: var(--font-family-monospace);
    font-size: var(--font-size-small);
  }
+
  .seeding-policy {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }

  .project-grid {
    display: grid;
@@ -138,21 +150,28 @@
        </div>
      </div>

-
      <div class="subtitle">
+
      <div class="subtitle" style:justify-content="space-between">
        <div class="txt-semibold">
          {isLocal(baseUrl.hostname) ? "Seeded" : "Pinned"} projects
        </div>
-
        <div class="global-hide-on-mobile-down" style:margin-left="auto">
+
        <div class="seeding-policy">
          {#if policy && scope}
-
            <ScopePolicyPopover {scope} {policy} popoverPositionRight="0" />
+
            <span class="txt-bold">Seeding Policy:</span>
+
            {capitalize(shortScope)}
+
            <div class="global-hide-on-mobile-down">
+
              <Popover
+
                popoverPositionBottom="0"
+
                popoverPositionLeft="-17rem"
+
                popoverPositionRight="2rem">
+
                <IconButton slot="toggle" let:toggle on:click={toggle}>
+
                  <IconSmall name="help" />
+
                </IconButton>
+
                <ScopePolicyExplainer slot="popover" {scope} {policy} />
+
              </Popover>
+
            </div>
          {/if}
        </div>
      </div>
-
      <div class="subtitle global-hide-on-small-desktop-up">
-
        {#if policy && scope}
-
          <ScopePolicyPopover {scope} {policy} popoverPositionRight="-4.5rem" />
-
        {/if}
-
      </div>

      <div style:margin-top="1rem" style:padding-bottom="2.5rem">
        {#await fetchProjectInfos( baseUrl, { show: isLocal(baseUrl.hostname) ? "all" : "pinned", perPage: stats.repos.total }, )}
modified src/views/projects/Commit.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Commit, Project } from "@httpd-client";
+
  import type { BaseUrl, Commit, Node, Project } from "@httpd-client";

  import { formatCommit } from "@app/lib/utils";

@@ -14,6 +14,7 @@
  import Share from "./Share.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let commit: Commit;
  export let project: Project;

@@ -46,7 +47,7 @@
  }
</style>

-
<Layout {baseUrl} {project}>
+
<Layout {node} {baseUrl} {project}>
  <div class="commit">
    <div class="header">
      <div style="display:flex; flex-direction: column; gap: 0.5rem;">
modified src/views/projects/History.svelte
@@ -4,6 +4,7 @@
    CommitHeader,
    Project,
    Remote,
+
    Node,
    Tree,
  } from "@httpd-client";
  import type { Route } from "@app/lib/router";
@@ -23,6 +24,7 @@
  import ProjectNameHeader from "./Source/ProjectNameHeader.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let commit: string;
  export let branches: string[];
  export let commitHeaders: CommitHeader[];
@@ -106,7 +108,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="source">
+
<Layout {node} {baseUrl} {project} activeTab="source">
  <ProjectNameHeader {project} {baseUrl} {seeding} slot="header" />

  <div style:margin="1rem 0 1rem 1rem" slot="subheader">
modified src/views/projects/Issue.svelte
@@ -7,6 +7,7 @@
    Issue,
    IssueState,
    Project,
+
    Node,
  } from "@httpd-client";
  import type { Session } from "@app/lib/httpd";

@@ -48,6 +49,7 @@
  import ThreadComponent from "@app/components/Thread.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let issue: Issue;
  export let project: Project;
  export let rawPath: (commit?: string) => string;
@@ -482,7 +484,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="issues" stylePaddingBottom="0">
+
<Layout {node} {baseUrl} {project} activeTab="issues" stylePaddingBottom="0">
  <div class="issue">
    <div class="main">
      <CobHeader>
modified src/views/projects/Issue/New.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Embed, Project, Reaction } from "@httpd-client";
+
  import type { BaseUrl, Embed, Node, Project, Reaction } from "@httpd-client";

  import * as modal from "@app/lib/modal";
  import * as router from "@app/lib/router";
@@ -17,6 +17,7 @@
  import TextInput from "@app/components/TextInput.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let project: Project;
  export let rawPath: (commit?: string) => string;

@@ -99,7 +100,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="issues">
+
<Layout {node} {baseUrl} {project} activeTab="issues">
  {#if session}
    {@const session_ = session}
    <div class="form">
modified src/views/projects/Issues.svelte
@@ -1,5 +1,11 @@
<script lang="ts">
-
  import type { BaseUrl, Issue, IssueState, Project } from "@httpd-client";
+
  import type {
+
    BaseUrl,
+
    Issue,
+
    IssueState,
+
    Node,
+
    Project,
+
  } from "@httpd-client";

  import capitalize from "lodash/capitalize";
  import { HttpdClient } from "@httpd-client";
@@ -24,6 +30,7 @@
  import Share from "./Share.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let issues: Issue[];
  export let project: Project;
  export let state: IssueState["status"];
@@ -105,7 +112,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="issues">
+
<Layout {node} {baseUrl} {project} activeTab="issues">
  <div slot="header" style:display="flex" style:padding="1rem">
    <Popover
      popoverPadding="0"
modified src/views/projects/Layout.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import type { ActiveTab } from "./Header.svelte";
-
  import type { BaseUrl, Project } from "@httpd-client";
+
  import type { BaseUrl, Node, Project } from "@httpd-client";

  import AppHeader from "@app/App/Header.svelte";

@@ -11,6 +11,7 @@
  import Sidebar from "@app/views/projects/Sidebar.svelte";

  export let activeTab: ActiveTab | undefined = undefined;
+
  export let node: Node;
  export let baseUrl: BaseUrl;
  export let project: Project;
  export let stylePaddingBottom: string = "2.5rem";
@@ -65,11 +66,11 @@
  </div>

  <div class="sidebar global-hide-on-medium-desktop-down">
-
    <Sidebar {activeTab} {baseUrl} {project} />
+
    <Sidebar {node} {activeTab} {baseUrl} {project} />
  </div>

  <div class="sidebar global-hide-on-mobile-down global-hide-on-desktop-up">
-
    <Sidebar {activeTab} {baseUrl} {project} collapsedOnly />
+
    <Sidebar {node} {activeTab} {baseUrl} {project} collapsedOnly />
  </div>

  <div class="content" style:padding-bottom={stylePaddingBottom}>
modified src/views/projects/Patch.svelte
@@ -8,6 +8,7 @@
    PatchState,
    Revision,
    Diff,
+
    Node,
  } from "@httpd-client";

  interface Thread {
@@ -91,6 +92,7 @@
  import { closeFocused } from "@app/components/Popover.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let patch: Patch;
  export let stats: Diff["stats"];
  export let rawPath: (commit?: string) => string;
@@ -706,7 +708,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="patches" stylePaddingBottom="0">
+
<Layout {node} {baseUrl} {project} activeTab="patches" stylePaddingBottom="0">
  <div class="patch">
    <div class="main">
      <CobHeader>
modified src/views/projects/Patches.svelte
@@ -1,5 +1,11 @@
<script lang="ts">
-
  import type { BaseUrl, Patch, PatchState, Project } from "@httpd-client";
+
  import type {
+
    BaseUrl,
+
    Node,
+
    Patch,
+
    PatchState,
+
    Project,
+
  } from "@httpd-client";

  import { HttpdClient } from "@httpd-client";
  import capitalize from "lodash/capitalize";
@@ -25,6 +31,7 @@
  import Command from "@app/components/Command.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let patches: Patch[];
  export let project: Project;
  export let state: PatchState["status"];
@@ -120,7 +127,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="patches">
+
<Layout {node} {baseUrl} {project} activeTab="patches">
  <div slot="header" style:display="flex" style:padding="1rem">
    <Popover
      popoverPadding="0"
modified src/views/projects/Sidebar.svelte
@@ -1,15 +1,17 @@
<script lang="ts">
  import type { ActiveTab } from "./Header.svelte";
-
  import type { BaseUrl, Project } from "@httpd-client";
+
  import type { BaseUrl, Node, Project } from "@httpd-client";
+

+
  import { onMount } from "svelte";

  import { experimental } from "@app/lib/appearance";
-
  import { queryProject } from "@app/lib/projects";
  import { httpdStore, api } from "@app/lib/httpd";
  import { isLocal } from "@app/lib/utils";
-
  import { onMount } from "svelte";
+
  import { queryProject } from "@app/lib/projects";

  import Button from "@app/components/Button.svelte";
  import ContextHelp from "@app/views/projects/Sidebar/ContextHelp.svelte";
+
  import ContextRepo from "@app/views/projects/Sidebar/ContextRepo.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import Link from "@app/components/Link.svelte";
  import Loading from "@app/components/Loading.svelte";
@@ -21,6 +23,7 @@
  const SIDEBAR_STATE_KEY = "sidebarState";

  export let activeTab: ActiveTab | undefined = undefined;
+
  export let node: Node;
  export let baseUrl: BaseUrl;
  export let project: Project;
  export let collapsedOnly = false;
@@ -129,17 +132,17 @@
    justify-content: space-between;
    width: 100%;
  }
+
  .repo,
  .help {
    z-index: 10;
-
    opacity: 0;
-
  }
-
  .help.expanded {
    opacity: 1;
-
    transition: opacity 150ms;
+
    transition:
+
      opacity 150ms,
+
      display 150ms allow-discrete;
    transition-delay: 150ms;
  }
  .help-box {
-
    width: 20.5rem;
+
    width: 100%;
    padding: 1rem;
    margin-bottom: 0.5rem;
    background-color: var(--color-background-float);
@@ -147,19 +150,20 @@
    font-size: var(--font-size-small);
    border-radius: var(--border-radius-small);
  }
+
  .repo-box {
+
    margin-bottom: 0.5rem;
+
  }
  .vertical-buttons {
    opacity: 1;
    height: fit-content;
    display: flex;
    flex-direction: column-reverse;
-
    transition: opacity 150ms ease-in-out;
+
    transition:
+
      opacity 150ms ease-in-out,
+
      display 150ms ease-in-out allow-discrete;
    transition-delay: 60ms;
    margin-bottom: 0.5rem;
  }
-
  .vertical-buttons.expanded {
-
    opacity: 0;
-
    height: 0;
-
  }
  .horizontal-buttons {
    display: flex;
    gap: 0.5rem;
@@ -170,6 +174,13 @@
    opacity: 1;
    transition: opacity 150ms ease-in-out;
  }
+
  .collapse-label {
+
    display: none;
+
  }
+
  .collapse-label.expanded {
+
    display: block;
+
    transition: opacity 30ms ease-in-out;
+
  }
  .icon {
    transform: rotate(180deg);
    transition: transform 150ms ease-in-out;
@@ -185,6 +196,7 @@
</style>

<div class="sidebar" class:expanded>
+
  <!-- Top Navigation Items -->
  <div class="project-navigation">
    <Link
      title="Source"
@@ -258,87 +270,113 @@
      </Button>
    </Link>
  </div>
+
  <!-- Context and other information section -->
  <div class="bottom">
-
    <div class="help" class:expanded>
-
      {#if !hideContextHelp && expanded && $experimental}
-
        {#if !localProject}
-
          <div
-
            style="display: flex; justify-content: center; align-items: center; height: 2rem;">
-
            <Loading small />
-
          </div>
-
        {:else}
-
          <div class="help-box">
-
            <ContextHelp
-
              {localProject}
-
              {baseUrl}
-
              projectId={project.id}
-
              hideLocalButton={isLocal(baseUrl.hostname) ||
-
                localProject !== "found"}
-
              disableLocalButton={$httpdStore.state !== "authenticated"} />
-
          </div>
+
    {#if expanded}
+
      <div class="help">
+
        {#if !hideContextHelp && $experimental}
+
          {#if !localProject}
+
            <div
+
              style="display: flex; justify-content: center; align-items: center; height: 2rem;">
+
              <Loading small />
+
            </div>
+
          {:else}
+
            <div class="help-box">
+
              <ContextHelp
+
                {localProject}
+
                {baseUrl}
+
                projectId={project.id}
+
                hideLocalButton={isLocal(baseUrl.hostname) ||
+
                  localProject !== "found"}
+
                disableLocalButton={$httpdStore.state !== "authenticated"} />
+
            </div>
+
          {/if}
        {/if}
-
      {/if}
-
    </div>
-
    <div class="vertical-buttons" class:expanded style:gap="0.5rem">
-
      <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
-
        <Button
-
          stylePadding="0 0.75rem"
-
          variant="background"
-
          title="Settings"
-
          slot="toggle"
-
          let:toggle
-
          on:click={toggle}>
-
          <IconSmall name="settings" />
-
        </Button>
+
      </div>
+
      <div class="repo">
+
        <div class="repo-box">
+
          <ContextRepo {project} {baseUrl} {node} />
+
        </div>
+
      </div>
+
    {:else}
+
      <div class="vertical-buttons" style:gap="0.5rem">
+
        <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
+
          <Button
+
            stylePadding="0 0.75rem"
+
            variant="background"
+
            title="Settings"
+
            slot="toggle"
+
            let:toggle
+
            on:click={toggle}>
+
            <IconSmall name="settings" />
+
          </Button>

-
        <Settings slot="popover" />
-
      </Popover>
+
          <Settings slot="popover" />
+
        </Popover>

-
      <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
-
        <Button
-
          stylePadding="0 0.75rem"
-
          variant="background"
-
          title="Help"
-
          slot="toggle"
-
          let:toggle
-
          on:click={toggle}>
-
          <IconSmall name="help" />
-
        </Button>
+
        <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
+
          <Button
+
            stylePadding="0 0.75rem"
+
            variant="background"
+
            title="Help"
+
            slot="toggle"
+
            let:toggle
+
            on:click={toggle}>
+
            <IconSmall name="help" />
+
          </Button>
+

+
          <Help slot="popover" />
+
        </Popover>

-
        <Help slot="popover" />
-
      </Popover>
+
        <Popover popoverPositionBottom="0" popoverPositionLeft="2rem">
+
          <Button
+
            stylePadding="0 0.75rem"
+
            variant="background"
+
            title="Info"
+
            slot="toggle"
+
            let:toggle
+
            on:click={toggle}>
+
            <IconSmall name="info" />
+
          </Button>

-
      {#if !hideContextHelp && $experimental}
-
        {#if !localProject}
-
          <div
-
            style="display: flex; justify-content: center; align-items: center; height: 2rem;">
-
            <Loading small condensed />
+
          <div slot="popover">
+
            <ContextRepo disablePopovers {node} {baseUrl} {project} />
          </div>
-
        {:else}
-
          <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
-
            <Button
-
              stylePadding="0 0.75rem"
-
              variant="background"
-
              title="Local node"
-
              slot="toggle"
-
              let:toggle
-
              on:click={toggle}>
-
              <IconSmall name="device" />
-
            </Button>
+
        </Popover>

-
            <ContextHelp
-
              {localProject}
-
              {baseUrl}
-
              popover
-
              projectId={project.id}
-
              hideLocalButton={isLocal(baseUrl.hostname) ||
-
                localProject !== "found"}
-
              disableLocalButton={$httpdStore.state !== "authenticated"}
-
              slot="popover" />
-
          </Popover>
+
        {#if !hideContextHelp && $experimental}
+
          {#if !localProject}
+
            <div
+
              style="display: flex; justify-content: center; align-items: center; height: 2rem;">
+
              <Loading small condensed />
+
            </div>
+
          {:else}
+
            <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
+
              <Button
+
                stylePadding="0 0.75rem"
+
                variant="background"
+
                title="Local node"
+
                slot="toggle"
+
                let:toggle
+
                on:click={toggle}>
+
                <IconSmall name="device" />
+
              </Button>
+

+
              <ContextHelp
+
                {localProject}
+
                {baseUrl}
+
                popover
+
                projectId={project.id}
+
                hideLocalButton={isLocal(baseUrl.hostname) ||
+
                  localProject !== "found"}
+
                disableLocalButton={$httpdStore.state !== "authenticated"}
+
                slot="popover" />
+
            </Popover>
+
          {/if}
        {/if}
-
      {/if}
-
    </div>
+
      </div>
+
    {/if}
+
    <!-- Footer -->
    {#if !collapsedOnly}
      <div class="sidebar-footer" style:flex-direction="row">
        <Button
@@ -348,8 +386,8 @@
          <div class="icon" class:expanded>
            <IconSmall name="chevron-left" />
          </div>
+
          <span class="collapse-label" class:expanded>Collapse</span>
        </Button>
-
        <div style:width="1.5rem" />
        <div class="horizontal-buttons" class:expanded>
          <Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
            <Button
@@ -364,6 +402,8 @@

            <Settings slot="popover" />
          </Popover>
+
        </div>
+
        <div class="horizontal-buttons" class:expanded>
          <Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
            <Button
              variant="background"
modified src/views/projects/Sidebar/ContextHelp.svelte
@@ -38,35 +38,49 @@
  }
  .title {
    padding-bottom: 0.75rem;
+
    text-wrap: nowrap;
+
    overflow: hidden;
+
  }
+
  .description {
+
    text-wrap: nowrap;
+
    overflow: hidden;
  }
</style>

<div class="help" class:popover>
  {#if $httpdStore.state === "stopped"}
    <div class="title txt-bold">Local node not connected</div>
-
    <div>Click the Connect button in the top right corner to get started.</div>
+
    <div class="description">
+
      Click the Connect button in the top right
+
      <br />
+
      corner to get started.
+
    </div>
  {:else if localProject === "notFound"}
    <div class="title txt-bold">Project not available locally</div>
-
    <div style:padding-bottom="0.5rem">
-
      This project hasn't been found on your local node. To get a local copy
-
      start seeding it using the following command.
+
    <div class="description" style:padding-bottom="0.5rem">
+
      This project hasn't been found on your local
+
      <br />
+
      node. To get a local copy start seeding it
+
      <br />
+
      using the following command.
    </div>
    <Command command={`rad seed ${projectId}`} />
  {:else if $httpdStore.state === "running" && localProject === "found"}
    <div class="title txt-bold">Not authenticated</div>
-
    <div>To make changes you need to authenticate yourself.</div>
-
    <div>
-
      Click the Authenticate button in the top right corner to get
-
      authenticated.
+
    <div class="description">To make changes you need to authenticate.</div>
+
    <div class="description">
+
      Click the Authenticate button in the top
+
      <br />
+
      right corner to get authenticated.
    </div>
  {:else if !isLocal(baseUrl.hostname) && localProject === "found"}
    <div class="title txt-bold">Read Only</div>
-
    <div>This is a read only preview hosted on</div>
+
    <div class="description">This is a read only preview hosted on</div>
    <ExternalLink href={baseUrl.hostname} />
  {/if}

  {#if !hideLocalButton}
-
    <div style:padding-top="1rem">
+
    <div class="txt-overflow" style:padding-top="1rem">
      <Link {route} disabled={disableLocalButton}>
        <Button size="large" styleWidth="100%" disabled={disableLocalButton}>
          <IconSmall name="device" />Make changes on your local node
added src/views/projects/Sidebar/ContextRepo.svelte
@@ -0,0 +1,129 @@
+
<script lang="ts">
+
  import type { BaseUrl, Node, Project } from "@httpd-client";
+

+
  import IconButton from "@app/components/IconButton.svelte";
+
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";
+
  import Popover from "@app/components/Popover.svelte";
+
  import ScopePolicyExplainer from "@app/components/ScopePolicyExplainer.svelte";
+
  import { isLocal } from "@app/lib/utils";
+
  import { capitalize } from "lodash";
+

+
  export let disablePopovers: boolean = false;
+
  export let project: Project;
+
  export let baseUrl: BaseUrl;
+
  export let node: Node;
+

+
  let expandedNode = false;
+

+
  $: shortSeedingPolicy =
+
    node.config?.scope === "all" && node.config?.policy === "allow"
+
      ? "permissive"
+
      : "restrictive";
+
</script>
+

+
<style>
+
  .nids {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1rem;
+
    padding: 0.5rem 0;
+
  }
+

+
  .node {
+
    display: flex;
+
    margin-top: 0.5rem;
+
    flex-direction: column;
+
    gap: 1rem;
+
    padding-top: 0.5rem;
+
  }
+
  .policies {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.5rem;
+
  }
+
  .item {
+
    display: flex;
+
    flex-wrap: nowrap;
+
    align-items: center;
+
    justify-content: space-between;
+
    gap: 0.5rem;
+
  }
+
  .no-wrap {
+
    text-wrap: nowrap;
+
  }
+
</style>
+

+
<div class="delegates txt-small">
+
  <div class="item" style:height="2rem">
+
    <div class="item">
+
      <IconSmall name="badge" />
+
      <span class="txt-bold">Delegates</span>
+
      <span class="txt-missing">{project.delegates.length}</span>
+
    </div>
+
    <div class="item">
+
      <IconSmall name="quorum" />
+
      <span class="txt-bold">
+
        {project.threshold}/{project.delegates.length}
+
      </span>
+
      {#if !disablePopovers}
+
        <Popover
+
          popoverPositionBottom="0"
+
          popoverPositionLeft="2rem"
+
          popoverPositionRight="-17rem">
+
          <IconButton slot="toggle" let:toggle on:click={toggle}>
+
            <IconSmall name="help" />
+
          </IconButton>
+

+
          <div slot="popover">
+
            {project.threshold} out of {project.delegates.length} delegates have
+
            to accept changes to be included in the canonical branch.
+
          </div>
+
        </Popover>
+
      {/if}
+
    </div>
+
  </div>
+
  <div class="nids">
+
    {#each project.delegates as { id: nodeId, alias }}
+
      <div style:width="fit-content">
+
        <NodeId {alias} {nodeId} stylePopoverPositionLeft="-0.8rem" />
+
      </div>
+
    {/each}
+
  </div>
+
</div>
+
<div class="txt-small node">
+
  <div class="item no-wrap txt-bold" style="justify-content: flex-start; ">
+
    {#if isLocal(baseUrl.hostname)}
+
      <IconSmall name="device" />Local Node
+
    {:else}
+
      <IconSmall name="seedling" />{baseUrl.hostname}
+
    {/if}
+
  </div>
+

+
  <div class="policies">
+
    <div class="item">
+
      <div class="item" style="justify-content: flex-start;">
+
        <IconButton on:click={() => (expandedNode = !expandedNode)}>
+
          <IconSmall name={`chevron-${expandedNode ? "down" : "right"}`} />
+
        </IconButton>
+
        <span class="no-wrap">Seeding Policy</span>
+
      </div>
+
      <div class="txt-bold">
+
        {capitalize(shortSeedingPolicy)}
+
      </div>
+
    </div>
+
    {#if expandedNode && node.config}
+
      <div style:padding-left="2.3rem">
+
        <ScopePolicyExplainer
+
          scope={node.config.scope}
+
          policy={node.config.policy} />
+
      </div>
+
    {/if}
+
    <div class="item txt-overflow" style:padding-top="0.5rem">
+
      Radicle version
+
      <span class="txt-missing txt-overflow txt-monospace" title={node.version}>
+
        {node.version}
+
      </span>
+
    </div>
+
  </div>
+
</div>
modified src/views/projects/Source.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Project, Remote, Tree } from "@httpd-client";
+
  import type { BaseUrl, Node, Project, Remote, Tree } from "@httpd-client";
  import type { BlobResult } from "./router";
  import type { Route } from "@app/lib/router";

@@ -15,6 +15,7 @@
  import ProjectNameHeader from "./Source/ProjectNameHeader.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let commit: string;
  export let rawPath: (commit?: string) => string;
  export let blobResult: BlobResult;
@@ -130,7 +131,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="source" stylePaddingBottom="0">
+
<Layout {node} {baseUrl} {project} activeTab="source" stylePaddingBottom="0">
  <ProjectNameHeader {project} {baseUrl} {seeding} slot="header" />

  <div style:margin="1rem 0 1rem 1rem" slot="subheader">
modified src/views/projects/router.ts
@@ -13,6 +13,7 @@ import type {
  DiffBlob,
  Issue,
  IssueState,
+
  Node,
  Patch,
  PatchState,
  Project,
@@ -113,6 +114,7 @@ export type ProjectLoadedRoute =
      resource: "project.source";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        commit: string;
        project: Project;
        peers: Remote[];
@@ -130,6 +132,7 @@ export type ProjectLoadedRoute =
      resource: "project.history";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        commit: string;
        project: Project;
        peers: Remote[];
@@ -145,6 +148,7 @@ export type ProjectLoadedRoute =
      resource: "project.commit";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        project: Project;
        commit: Commit;
      };
@@ -153,6 +157,7 @@ export type ProjectLoadedRoute =
      resource: "project.issue";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        project: Project;
        rawPath: (commit?: string) => string;
        issue: Issue;
@@ -162,6 +167,7 @@ export type ProjectLoadedRoute =
      resource: "project.issues";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        project: Project;
        issues: Issue[];
        state: IssueState["status"];
@@ -171,6 +177,7 @@ export type ProjectLoadedRoute =
      resource: "project.newIssue";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        project: Project;
        rawPath: (commit?: string) => string;
      };
@@ -179,6 +186,7 @@ export type ProjectLoadedRoute =
      resource: "project.patches";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        project: Project;
        patches: Patch[];
        state: PatchState["status"];
@@ -188,6 +196,7 @@ export type ProjectLoadedRoute =
      resource: "project.patch";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        project: Project;
        rawPath: (commit?: string) => string;
        patch: Patch;
@@ -270,6 +279,16 @@ export async function loadProjectRoute(
  previousLoaded: LoadedRoute,
): Promise<ProjectLoadedRoute | ErrorRoute | NotFoundRoute> {
  const api = new HttpdClient(route.node);
+
  let node: Node | undefined = undefined;
+
  if (
+
    "params" in previousLoaded &&
+
    "node" in previousLoaded.params &&
+
    route.node.hostname === previousLoaded.params.baseUrl.hostname
+
  ) {
+
    node = previousLoaded.params.node;
+
  } else {
+
    node = await api.getNode();
+
  }
  const rawPath = (commit?: string) =>
    `${route.node.scheme}://${route.node.hostname}:${route.node.port}/raw/${
      route.project
@@ -277,9 +296,9 @@ export async function loadProjectRoute(

  try {
    if (route.resource === "project.source") {
-
      return await loadTreeView(route, previousLoaded);
+
      return await loadTreeView(route, previousLoaded, node);
    } else if (route.resource === "project.history") {
-
      return await loadHistoryView(route);
+
      return await loadHistoryView(route, node);
    } else if (route.resource === "project.commit") {
      const [project, commit] = await Promise.all([
        api.project.getById(route.project),
@@ -290,28 +309,30 @@ export async function loadProjectRoute(
        resource: "project.commit",
        params: {
          baseUrl: route.node,
+
          node,
          project,
          commit,
        },
      };
    } else if (route.resource === "project.issue") {
-
      return await loadIssueView(route);
+
      return await loadIssueView(route, node);
    } else if (route.resource === "project.patch") {
-
      return await loadPatchView(route);
+
      return await loadPatchView(route, node);
    } else if (route.resource === "project.issues") {
-
      return await loadIssuesView(route);
+
      return await loadIssuesView(route, node);
    } else if (route.resource === "project.newIssue") {
      const project = await api.project.getById(route.project);
      return {
        resource: "project.newIssue",
        params: {
          baseUrl: route.node,
+
          node,
          project,
          rawPath,
        },
      };
    } else if (route.resource === "project.patches") {
-
      return await loadPatchesView(route);
+
      return await loadPatchesView(route, node);
    } else {
      return unreachable(route);
    }
@@ -330,6 +351,7 @@ export async function loadProjectRoute(

async function loadPatchesView(
  route: ProjectPatchesRoute,
+
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
  const searchParams = new URLSearchParams(route.search || "");
@@ -348,6 +370,7 @@ async function loadPatchesView(
    resource: "project.patches",
    params: {
      baseUrl: route.node,
+
      node,
      patches,
      state,
      project,
@@ -357,6 +380,7 @@ async function loadPatchesView(

async function loadIssuesView(
  route: ProjectIssuesRoute,
+
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
  const state = route.state || "open";
@@ -375,6 +399,7 @@ async function loadIssuesView(
    resource: "project.issues",
    params: {
      baseUrl: route.node,
+
      node,
      issues,
      state,
      project,
@@ -385,6 +410,7 @@ async function loadIssuesView(
async function loadTreeView(
  route: ProjectTreeRoute,
  previousLoaded: LoadedRoute,
+
  node: Node,
): Promise<ProjectLoadedRoute | NotFoundRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
@@ -450,6 +476,7 @@ async function loadTreeView(
    resource: "project.source",
    params: {
      baseUrl: route.node,
+
      node,
      commit,
      project,
      peers: peers.filter(remote => Object.keys(remote.heads).length > 0),
@@ -516,6 +543,7 @@ async function loadBlob(
}
async function loadHistoryView(
  route: ProjectHistoryRoute,
+
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);

@@ -554,6 +582,7 @@ async function loadHistoryView(
    resource: "project.history",
    params: {
      baseUrl: route.node,
+
      node,
      commit: commitId,
      project,
      peers: peers.filter(remote => Object.keys(remote.heads).length > 0),
@@ -569,6 +598,7 @@ async function loadHistoryView(

async function loadIssueView(
  route: ProjectIssueRoute,
+
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
@@ -584,6 +614,7 @@ async function loadIssueView(
    resource: "project.issue",
    params: {
      baseUrl: route.node,
+
      node,
      project,
      rawPath,
      issue,
@@ -593,6 +624,7 @@ async function loadIssueView(

async function loadPatchView(
  route: ProjectPatchRoute,
+
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
@@ -658,6 +690,7 @@ async function loadPatchView(
    resource: "project.patch",
    params: {
      baseUrl: route.node,
+
      node,
      project,
      rawPath,
      patch,