Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Simplify sidebar context boxes
Merged did:key:z6MkkfM3...sVz5 opened 1 year ago

Also starts removing features that relied on the web client handling local httpd instances

check check-visual check-unit-test check-http-client-unit-test check-radicle-httpd check-e2e check-build check-http

šŸ‘‰ Preview šŸ‘‰ Workflow runs šŸ‘‰ Branch on GitHub

19 files changed +277 -452 e4b15e72 → 16693451
modified http-client/index.ts
@@ -12,6 +12,7 @@ import type {
  SuccessResponse,
  CodeLocation,
  Range,
+
  SeedingPolicy,
  DefaultSeedingPolicy,
} from "./lib/shared.js";
import type { Comment, Embed, Reaction } from "./lib/project/comment.js";
@@ -50,6 +51,7 @@ import { Fetcher } from "./lib/fetcher.js";
import {
  nodeConfigSchema,
  scopeSchema,
+
  seedingPolicySchema,
  successResponseSchema,
} from "./lib/shared.js";

@@ -85,6 +87,7 @@ export type {
  Remote,
  Review,
  Revision,
+
  SeedingPolicy,
  TreeStats,
  Tree,
  Verdict,
@@ -211,14 +214,14 @@ export class HttpdClient {
  public async getPoliciesById(
    id: string,
    options?: RequestOptions,
-
  ): Promise<NodePolicies["policy"][]> {
+
  ): Promise<SeedingPolicy> {
    return this.#fetcher.fetchOk(
      {
        method: "GET",
        path: `node/policies/repos/${id}`,
        options,
      },
-
      array(nodePoliciesSchema.shape.policy),
+
      seedingPolicySchema,
    );
  }

modified http-client/lib/shared.ts
@@ -12,6 +12,18 @@ export const successResponseSchema = object({

export const scopeSchema = union([literal("followed"), literal("all")]);

+
export const seedingPolicySchema = union([
+
  object({
+
    policy: literal("block"),
+
  }),
+
  object({
+
    policy: literal("allow"),
+
    scope: scopeSchema,
+
  }),
+
]);
+

+
export type SeedingPolicy = z.infer<typeof seedingPolicySchema>;
+

const defaultSeedingPolicySchema = union([
  object({
    default: literal("block"),
modified src/components/ScopePolicyExplainer.svelte
@@ -1,9 +1,11 @@
<script lang="ts">
-
  import type { DefaultSeedingPolicy } from "@http-client";
+
  import type { DefaultSeedingPolicy, SeedingPolicy } from "@http-client";

  import { capitalize } from "lodash";

-
  export let seedingPolicy: DefaultSeedingPolicy;
+
  export let seedingPolicy: DefaultSeedingPolicy | SeedingPolicy;
+

+
  $: [policy, scope] = Object.values(seedingPolicy);
</script>

<style>
@@ -15,15 +17,15 @@
  }
</style>

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

<div class="section">
  Policy:
-
  <span class="txt-bold">{capitalize(seedingPolicy.default)}</span>
+
  <span class="txt-bold">{capitalize(policy)}</span>
</div>
<div class="txt-missing">
-
  {#if seedingPolicy.default === "allow"}
+
  {#if policy === "allow"}
    All discovered repositories will get seeded.
-
  {:else if seedingPolicy.default === "block"}
+
  {:else if policy === "block"}
    Only repositories marked as such will get seeded.
  {/if}
</div>
modified src/lib/utils.ts
@@ -1,4 +1,4 @@
-
import type { BaseUrl, DefaultSeedingPolicy } from "@http-client";
+
import type { BaseUrl } from "@http-client";

import md5 from "md5";
import bs58 from "bs58";
@@ -13,14 +13,6 @@ export function formatLocationHash(hash: string | null): number | null {
  return null;
}

-
export function formatShortSeedingPolicy(
-
  policy: DefaultSeedingPolicy | undefined,
-
) {
-
  return policy?.default === "allow" && policy?.scope === "all"
-
    ? "permissive"
-
    : "restrictive";
-
}
-

// Removes the first and last character which are always `/`.
export function formatUserAgent(agent: string): string {
  return agent.slice(1, -1);
modified src/views/nodes/View.svelte
@@ -7,7 +7,6 @@
  import { api, httpdStore } from "@app/lib/httpd";
  import {
    baseUrlToString,
-
    formatShortSeedingPolicy,
    formatUserAgent,
    isLocal,
    truncateId,
@@ -32,7 +31,10 @@
  export let seedingPolicy: DefaultSeedingPolicy | undefined = undefined;
  export let agent: string;

-
  $: shortScope = formatShortSeedingPolicy(seedingPolicy);
+
  $: shortScope =
+
    seedingPolicy?.default === "allow" && seedingPolicy?.scope === "all"
+
      ? "permissive"
+
      : "restrictive";
  $: hostname = isLocal(baseUrl.hostname) ? "Local Node" : baseUrl.hostname;
  $: session =
    $httpdStore.state === "authenticated" && isLocal(api.baseUrl.hostname)
modified src/views/projects/Commit.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Commit, Node, Project } from "@http-client";
+
  import type { BaseUrl, Commit, Project, SeedingPolicy } from "@http-client";

  import Button from "@app/components/Button.svelte";
  import Changeset from "@app/views/projects/Changeset.svelte";
@@ -12,7 +12,7 @@
  import Id from "@app/components/Id.svelte";

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

@@ -45,7 +45,7 @@
  }
</style>

-
<Layout {node} {baseUrl} {project}>
+
<Layout {seedingPolicy} {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,8 +4,8 @@
    CommitHeader,
    Project,
    Remote,
-
    Node,
    Tree,
+
    SeedingPolicy,
  } from "@http-client";
  import type { ProjectRoute } from "./router";

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

  export let baseUrl: BaseUrl;
-
  export let node: Node;
+
  export let seedingPolicy: SeedingPolicy;
  export let commit: string;
  export let commitHeaders: CommitHeader[];
  export let peer: string | undefined;
@@ -90,7 +90,7 @@
  }
</style>

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

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

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

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

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

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

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

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

-
<Layout {node} {baseUrl} {project} activeTab="issues">
+
<Layout {seedingPolicy} {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, Node, Project } from "@http-client";
+
  import type {
+
    BaseUrl,
+
    Issue,
+
    IssueState,
+
    Project,
+
    SeedingPolicy,
+
  } from "@http-client";

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

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

-
<Layout {node} {baseUrl} {project} activeTab="issues">
+
<Layout {seedingPolicy} {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, Node, Project } from "@http-client";
+
  import type { BaseUrl, Project, SeedingPolicy } from "@http-client";

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

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

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

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

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

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

  interface Thread {
@@ -92,7 +92,7 @@
  import TextInput from "@app/components/TextInput.svelte";

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

-
<Layout {node} {baseUrl} {project} activeTab="patches" stylePaddingBottom="0">
+
<Layout
+
  {seedingPolicy}
+
  {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, Node, Patch, PatchState, Project } from "@http-client";
+
  import type {
+
    BaseUrl,
+
    Patch,
+
    PatchState,
+
    Project,
+
    SeedingPolicy,
+
  } from "@http-client";

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

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

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

-
  import { onMount } from "svelte";
-

-
  import { experimental } from "@app/lib/appearance";
-
  import { httpdStore, api } from "@app/lib/httpd";
-
  import { isLocal } from "@app/lib/utils";
-
  import { queryProject } from "@app/lib/projects";
+
  import type { BaseUrl, Project, SeedingPolicy } from "@http-client";

  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 Help from "@app/App/Help.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import Link from "@app/components/Link.svelte";
-
  import Loading from "@app/components/Loading.svelte";
  import Popover from "@app/components/Popover.svelte";
-

-
  import Help from "@app/App/Help.svelte";
  import Settings from "@app/App/Settings.svelte";

  const SIDEBAR_STATE_KEY = "sidebarState";

  export let activeTab: ActiveTab | undefined = undefined;
-
  export let node: Node;
+
  export let seedingPolicy: SeedingPolicy;
  export let baseUrl: BaseUrl;
  export let project: Project;
  export let collapsedOnly = false;
@@ -37,20 +27,6 @@
    );
  }

-
  // To avoid concurrent request.
-
  let queryingLocalProject: boolean = true;
-
  let localProject: "notFound" | "found" | undefined = undefined;
-
  $: hideContextHelp =
-
    $experimental &&
-
    isLocal(baseUrl.hostname) &&
-
    $httpdStore.state === "authenticated";
-

-
  httpdStore.subscribe(async () => {
-
    if ($httpdStore.state !== "stopped" && !queryingLocalProject) {
-
      await detectLocalProject();
-
    }
-
  });
-

  function loadSidebarState(): boolean {
    const storedSidebarState = window.localStorage.getItem(SIDEBAR_STATE_KEY);

@@ -65,20 +41,6 @@
    expanded = !expanded;
    storeSidebarState(expanded);
  }
-

-
  async function detectLocalProject(): Promise<void> {
-
    queryingLocalProject = true;
-
    localProject = await queryProject(api.baseUrl, project.id);
-
    queryingLocalProject = false;
-
  }
-

-
  onMount(async () => {
-
    if ($httpdStore.state !== "stopped") {
-
      await detectLocalProject();
-
    } else {
-
      localProject = "notFound";
-
    }
-
  });
</script>

<style>
@@ -132,17 +94,13 @@
    justify-content: space-between;
    width: 100%;
  }
-
  .repo,
-
  .help {
+
  .repo {
    z-index: 10;
-
    opacity: 1;
-
    transition:
-
      opacity 150ms,
-
      display 150ms allow-discrete;
-
    transition-delay: 150ms;
+
    opacity: 0;
+
    height: 0;
+
    overflow: hidden;
  }
-
  .help-box {
-
    width: 100%;
+
  .box {
    padding: 1rem;
    margin-bottom: 0.5rem;
    background-color: var(--color-background-float);
@@ -150,20 +108,27 @@
    font-size: var(--font-size-small);
    border-radius: var(--border-radius-small);
  }
-
  .repo-box {
-
    margin-bottom: 0.5rem;
+
  .repo.expanded {
+
    opacity: 1;
+
    height: initial;
+
    overflow: initial;
+
    transition: opacity 150ms;
+
    transition-delay: 150ms;
  }
  .vertical-buttons {
    opacity: 1;
    height: fit-content;
    display: flex;
    flex-direction: column-reverse;
-
    transition:
-
      opacity 150ms ease-in-out,
-
      display 150ms ease-in-out allow-discrete;
+
    transition: opacity 150ms ease-in-out;
    transition-delay: 60ms;
    margin-bottom: 0.5rem;
  }
+
  .vertical-buttons.expanded {
+
    opacity: 0;
+
    height: 0;
+
    overflow: hidden;
+
  }
  .horizontal-buttons {
    display: flex;
    gap: 0.5rem;
@@ -210,7 +175,7 @@
        stylePadding="0.5rem 0.75rem"
        size="large"
        styleWidth="100%"
-
        styleJustifyContent={"flex-start"}
+
        styleJustifyContent="flex-start"
        variant={activeTab === "source" ? "gray" : "background"}>
        <IconSmall name="chevron-left-right" />
        <span class="title-counter" class:expanded>Source</span>
@@ -227,7 +192,7 @@
        stylePadding="0.5rem 0.75rem"
        let:hover
        size="large"
-
        styleJustifyContent={"flex-start"}
+
        styleJustifyContent="flex-start"
        styleWidth="100%"
        variant={activeTab === "issues" ? "gray" : "background"}>
        <IconSmall name="issue" />
@@ -255,7 +220,7 @@
        let:hover
        size="large"
        styleWidth="100%"
-
        styleJustifyContent={"flex-start"}
+
        styleJustifyContent="flex-start"
        variant={activeTab === "patches" ? "gray" : "background"}>
        <IconSmall name="patch" />
        <div class="title-counter" class:expanded>
@@ -272,110 +237,60 @@
  </div>
  <!-- Context and other information section -->
  <div class="bottom">
-
    {#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}
-
      </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>
-

-
        <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>
+
    <div class="repo box" class:expanded>
+
      <ContextRepo
+
        projectThreshold={project.threshold}
+
        projectDelegates={project.delegates}
+
        {seedingPolicy} />
+
    </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>

-
          <Help slot="popover" />
-
        </Popover>
+
        <Settings 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>
+
      <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>

-
          <div slot="popover">
-
            <ContextRepo disablePopovers {node} {baseUrl} {project} />
-
          </div>
-
        </Popover>
+
        <Help 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>
+
      <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
+
        <Button
+
          stylePadding="0 0.75rem"
+
          variant="background"
+
          title="Info"
+
          slot="toggle"
+
          let:toggle
+
          on:click={toggle}>
+
          <IconSmall name="info" />
+
        </Button>

-
              <ContextHelp
-
                {localProject}
-
                {baseUrl}
-
                popover
-
                projectId={project.id}
-
                hideLocalButton={isLocal(baseUrl.hostname) ||
-
                  localProject !== "found"}
-
                disableLocalButton={$httpdStore.state !== "authenticated"}
-
                slot="popover" />
-
            </Popover>
-
          {/if}
-
        {/if}
-
      </div>
-
    {/if}
+
        <div slot="popover" class="txt-small" style:width="18rem">
+
          <ContextRepo
+
            projectThreshold={project.threshold}
+
            projectDelegates={project.delegates}
+
            {seedingPolicy} />
+
        </div>
+
      </Popover>
+
    </div>
    <!-- Footer -->
    {#if !collapsedOnly}
      <div class="sidebar-footer" style:flex-direction="row">
deleted src/views/projects/Sidebar/ContextHelp.svelte
@@ -1,91 +0,0 @@
-
<script lang="ts">
-
  import type { BaseUrl } from "@http-client";
-
  import type { Route } from "@app/lib/router/definitions";
-

-
  import { activeUnloadedRouteStore } from "@app/lib/router";
-
  import { api, httpdStore } from "@app/lib/httpd";
-
  import { isLocal } from "@app/lib/utils";
-

-
  import Button from "@app/components/Button.svelte";
-
  import Command from "@app/components/Command.svelte";
-
  import ExternalLink from "@app/components/ExternalLink.svelte";
-
  import IconSmall from "@app/components/IconSmall.svelte";
-
  import Link from "@app/components/Link.svelte";
-

-
  export let baseUrl: BaseUrl;
-
  export let disableLocalButton: boolean;
-
  export let hideLocalButton: boolean;
-
  export let localProject: "notFound" | "found" | undefined;
-
  export let projectId: string;
-
  export let popover: boolean = false;
-

-
  let route: Route;
-

-
  $: {
-
    route = $activeUnloadedRouteStore;
-
    if ("node" in route) {
-
      route.node = api.baseUrl;
-
    }
-
  }
-
</script>
-

-
<style>
-
  .help {
-
    font-size: var(--font-size-small);
-
  }
-
  .popover {
-
    width: 18.5rem;
-
  }
-
  .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 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">Repository not available locally</div>
-
    <div class="description" style:padding-bottom="0.5rem">
-
      This repository 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 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 class="description">This is a read only preview hosted on</div>
-
    <ExternalLink href={baseUrl.hostname} />
-
  {/if}
-

-
  {#if !hideLocalButton}
-
    <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
-
        </Button>
-
      </Link>
-
    </div>
-
  {/if}
-
</div>
modified src/views/projects/Sidebar/ContextRepo.svelte
@@ -1,119 +1,88 @@
<script lang="ts">
-
  import type { BaseUrl, Node, Project } from "@http-client";
+
  import type { Project, SeedingPolicy } from "@http-client";

  import { capitalize } from "lodash";
-
  import { formatShortSeedingPolicy, isLocal } from "@app/lib/utils";

  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";

-
  export let disablePopovers: boolean = false;
-
  export let project: Project;
-
  export let baseUrl: BaseUrl;
-
  export let node: Node;
+
  export let projectThreshold: number;
+
  export let projectDelegates: Project["delegates"];
+
  export let seedingPolicy: SeedingPolicy;

-
  let expandedNode = false;
-

-
  $: shortSeedingPolicy = formatShortSeedingPolicy(node.config?.seedingPolicy);
+
  let delegateExpanded = false;
+
  let policyExpanded = false;
</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 {
+
  .item-header {
+
    gap: 2rem;
    display: flex;
-
    flex-wrap: nowrap;
    align-items: center;
    justify-content: space-between;
-
    gap: 0.5rem;
+
    margin: 0.2rem 0;
+
  }
+
  .item-header:first-child {
+
    margin-top: 0;
+
  }
+
  .item-header:last-child {
+
    margin-bottom: 0;
  }
-
  .no-wrap {
-
    text-wrap: nowrap;
+
  .nid {
+
    margin: 0.5rem 0;
  }
</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} />
-
      </div>
-
    {/each}
+
<div class="item-header">
+
  <span>Delegates</span>
+
  <div class="global-flex-item">
+
    <span class="txt-bold">
+
      {projectThreshold}/{projectDelegates.length}
+
    </span>
+
    <IconButton on:click={() => (delegateExpanded = !delegateExpanded)}>
+
      <IconSmall name={delegateExpanded ? "chevron-up" : "chevron-down"} />
+
    </IconButton>
  </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
+
{#if delegateExpanded}
+
  <div style:color="var(--color-foreground-dim" style:margin-bottom="1rem">
+
    {#if projectDelegates.length === 1}
+
      Any changes accepted by the sole delegate will be included in the
+
      canonical branch.
    {:else}
-
      <IconSmall name="seedling" />{baseUrl.hostname}
+
      {projectThreshold} out of {projectDelegates.length} delegates have to accept
+
      changes to be included in the canonical branch.
    {/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>
+
  {#each projectDelegates as delegate}
+
    <div class="nid">
+
      <NodeId nodeId={delegate.id} alias={delegate.alias} />
    </div>
-
    {#if expandedNode && node.config?.seedingPolicy}
-
      <div style:padding-left="2.3rem">
-
        <ScopePolicyExplainer seedingPolicy={node.config.seedingPolicy} />
-
      </div>
-
    {/if}
+
  {/each}
+
{/if}
+
<div class="item-header">
+
  <span style:text-wrap="nowrap">Seeding Scope</span>
+
  <div class="global-flex-item">
+
    <span class="txt-bold">
+
      {capitalize(
+
        "scope" in seedingPolicy ? seedingPolicy.scope : "not defined",
+
      )}
+
    </span>
+
    <IconButton on:click={() => (policyExpanded = !policyExpanded)}>
+
      <IconSmall name={policyExpanded ? "chevron-up" : "chevron-down"} />
+
    </IconButton>
  </div>
</div>
+
{#if policyExpanded}
+
  <div style:color="var(--color-foreground-dim)">
+
    {#if seedingPolicy.policy === "block"}
+
      Seeding scope only has an effect when a repository is seeded. This repo
+
      isn't seeded by the seed node.
+
    {:else if seedingPolicy.scope === "all"}
+
      This repository tracks changes by any peer.
+
    {:else}
+
      This repository tracks only peers followed by the seed node.
+
    {/if}
+
  </div>
+
{/if}
modified src/views/projects/Source.svelte
@@ -1,5 +1,11 @@
<script lang="ts">
-
  import type { BaseUrl, Node, Project, Remote, Tree } from "@http-client";
+
  import type {
+
    BaseUrl,
+
    Project,
+
    Remote,
+
    SeedingPolicy,
+
    Tree,
+
  } from "@http-client";
  import type { BlobResult, ProjectRoute } from "./router";

  import { HttpdClient } from "@http-client";
@@ -16,11 +22,11 @@
  export let baseUrl: BaseUrl;
  export let blobResult: BlobResult;
  export let commit: string;
-
  export let node: Node;
  export let path: string;
  export let peer: string | undefined;
  export let peers: Remote[];
  export let project: Project;
+
  export let seedingPolicy: SeedingPolicy;
  export let rawPath: (commit?: string) => string;
  export let revision: string | undefined;
  export let seeding: boolean;
@@ -111,7 +117,12 @@
  }
</style>

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

  <div style:margin="1rem" slot="subheader">
modified src/views/projects/router.ts
@@ -13,11 +13,11 @@ import type {
  DiffBlob,
  Issue,
  IssueState,
-
  Node,
  Patch,
  PatchState,
  Project,
  Remote,
+
  SeedingPolicy,
  Tree,
} from "@http-client";

@@ -114,7 +114,7 @@ export type ProjectLoadedRoute =
      resource: "project.source";
      params: {
        baseUrl: BaseUrl;
-
        node: Node;
+
        seedingPolicy: SeedingPolicy;
        commit: string;
        project: Project;
        peers: Remote[];
@@ -131,7 +131,7 @@ export type ProjectLoadedRoute =
      resource: "project.history";
      params: {
        baseUrl: BaseUrl;
-
        node: Node;
+
        seedingPolicy: SeedingPolicy;
        commit: string;
        project: Project;
        peers: Remote[];
@@ -146,7 +146,7 @@ export type ProjectLoadedRoute =
      resource: "project.commit";
      params: {
        baseUrl: BaseUrl;
-
        node: Node;
+
        seedingPolicy: SeedingPolicy;
        project: Project;
        commit: Commit;
      };
@@ -155,7 +155,7 @@ export type ProjectLoadedRoute =
      resource: "project.issue";
      params: {
        baseUrl: BaseUrl;
-
        node: Node;
+
        seedingPolicy: SeedingPolicy;
        project: Project;
        rawPath: (commit?: string) => string;
        issue: Issue;
@@ -165,7 +165,7 @@ export type ProjectLoadedRoute =
      resource: "project.issues";
      params: {
        baseUrl: BaseUrl;
-
        node: Node;
+
        seedingPolicy: SeedingPolicy;
        project: Project;
        issues: Issue[];
        state: IssueState["status"];
@@ -175,7 +175,7 @@ export type ProjectLoadedRoute =
      resource: "project.newIssue";
      params: {
        baseUrl: BaseUrl;
-
        node: Node;
+
        seedingPolicy: SeedingPolicy;
        project: Project;
        rawPath: (commit?: string) => string;
      };
@@ -184,7 +184,7 @@ export type ProjectLoadedRoute =
      resource: "project.patches";
      params: {
        baseUrl: BaseUrl;
-
        node: Node;
+
        seedingPolicy: SeedingPolicy;
        project: Project;
        patches: Patch[];
        state: PatchState["status"];
@@ -194,7 +194,7 @@ export type ProjectLoadedRoute =
      resource: "project.patch";
      params: {
        baseUrl: BaseUrl;
-
        node: Node;
+
        seedingPolicy: SeedingPolicy;
        project: Project;
        rawPath: (commit?: string) => string;
        patch: Patch;
@@ -277,16 +277,6 @@ 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
@@ -294,43 +284,47 @@ export async function loadProjectRoute(

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

      return {
        resource: "project.commit",
        params: {
          baseUrl: route.node,
-
          node,
+
          seedingPolicy,
          project,
          commit,
        },
      };
    } else if (route.resource === "project.issue") {
-
      return await loadIssueView(route, node);
+
      return await loadIssueView(route);
    } else if (route.resource === "project.patch") {
-
      return await loadPatchView(route, node);
+
      return await loadPatchView(route);
    } else if (route.resource === "project.issues") {
-
      return await loadIssuesView(route, node);
+
      return await loadIssuesView(route);
    } else if (route.resource === "project.newIssue") {
-
      const project = await api.project.getById(route.project);
+
      const [project, seedingPolicy] = await Promise.all([
+
        api.project.getById(route.project),
+
        api.getPoliciesById(route.project),
+
      ]);
      return {
        resource: "project.newIssue",
        params: {
          baseUrl: route.node,
-
          node,
+
          seedingPolicy,
          project,
          rawPath,
        },
      };
    } else if (route.resource === "project.patches") {
-
      return await loadPatchesView(route, node);
+
      return await loadPatchesView(route);
    } else {
      return unreachable(route);
    }
@@ -349,26 +343,26 @@ 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 || "");
  const state = (searchParams.get("state") as PatchState["status"]) || "open";

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

  return {
    resource: "project.patches",
    params: {
      baseUrl: route.node,
-
      node,
+
      seedingPolicy,
      patches,
      state,
      project,
@@ -378,26 +372,25 @@ async function loadPatchesView(

async function loadIssuesView(
  route: ProjectIssuesRoute,
-
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
  const state = route.state || "open";

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

  return {
    resource: "project.issues",
    params: {
      baseUrl: route.node,
-
      node,
+
      seedingPolicy,
      issues,
      state,
      project,
@@ -408,7 +401,6 @@ 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) =>
@@ -417,6 +409,7 @@ async function loadTreeView(
    }${commit ? `/${commit}` : ""}`;

  let projectPromise: Promise<Project>;
+
  let seedingPolicyPromise: Promise<SeedingPolicy>;
  let peersPromise: Promise<Remote[]>;
  if (
    previousLoaded.resource === "project.source" &&
@@ -425,14 +418,17 @@ async function loadTreeView(
  ) {
    projectPromise = Promise.resolve(previousLoaded.params.project);
    peersPromise = Promise.resolve(previousLoaded.params.peers);
+
    seedingPolicyPromise = Promise.resolve(previousLoaded.params.seedingPolicy);
  } else {
    projectPromise = api.project.getById(route.project);
    peersPromise = api.project.getAllRemotes(route.project);
+
    seedingPolicyPromise = api.getPoliciesById(route.project);
  }

-
  const [project, peers, seeding] = await Promise.all([
+
  const [project, peers, seedingPolicy, seeding] = await Promise.all([
    projectPromise,
    peersPromise,
+
    seedingPolicyPromise,
    isLocalNodeSeeding(route),
  ]);

@@ -471,7 +467,7 @@ async function loadTreeView(
    resource: "project.source",
    params: {
      baseUrl: route.node,
-
      node,
+
      seedingPolicy,
      commit,
      project,
      peers: peers.filter(remote => Object.keys(remote.heads).length > 0),
@@ -537,13 +533,13 @@ async function loadBlob(
}
async function loadHistoryView(
  route: ProjectHistoryRoute,
-
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);

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

@@ -576,7 +572,7 @@ async function loadHistoryView(
    resource: "project.history",
    params: {
      baseUrl: route.node,
-
      node,
+
      seedingPolicy,
      commit: commitId,
      project,
      peers: peers.filter(remote => Object.keys(remote.heads).length > 0),
@@ -591,7 +587,6 @@ async function loadHistoryView(

async function loadIssueView(
  route: ProjectIssueRoute,
-
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
@@ -599,15 +594,16 @@ async function loadIssueView(
      route.project
    }${commit ? `/${commit}` : ""}`;

-
  const [project, issue] = await Promise.all([
+
  const [project, issue, seedingPolicy] = await Promise.all([
    api.project.getById(route.project),
    api.project.getIssueById(route.project, route.issue),
+
    api.getPoliciesById(route.project),
  ]);
  return {
    resource: "project.issue",
    params: {
      baseUrl: route.node,
-
      node,
+
      seedingPolicy,
      project,
      rawPath,
      issue,
@@ -617,7 +613,6 @@ async function loadIssueView(

async function loadPatchView(
  route: ProjectPatchRoute,
-
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
@@ -625,9 +620,10 @@ async function loadPatchView(
      route.project
    }${commit ? `/${commit}` : ""}`;

-
  const [project, patch] = await Promise.all([
+
  const [project, patch, seedingPolicy] = await Promise.all([
    api.project.getById(route.project),
    api.project.getPatchById(route.project, route.patch),
+
    api.getPoliciesById(route.project),
  ]);
  const latestRevision = patch.revisions[patch.revisions.length - 1];
  const { diff } = await api.project.getDiff(
@@ -683,7 +679,7 @@ async function loadPatchView(
    resource: "project.patch",
    params: {
      baseUrl: route.node,
-
      node,
+
      seedingPolicy,
      project,
      rawPath,
      patch,
modified tests/unit/utils.test.ts
@@ -11,20 +11,6 @@ describe("Format functions", () => {

  test.each([
    {
-
      input: { default: "allow", scope: "all" },
-
      expected: "permissive",
-
    } as const,
-
    {
-
      input: { default: "allow", scope: "followed" },
-
      expected: "restrictive",
-
    } as const,
-
    { input: { default: "block" }, expected: "restrictive" } as const,
-
  ])("formatShortSeedingPolicy $input => $expected", ({ input, expected }) => {
-
    expect(utils.formatShortSeedingPolicy(input)).toEqual(expected);
-
  });
-

-
  test.each([
-
    {
      id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
      expected: "rad:zKtT7D…19WzjT",
    },