Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Use generic routing Link component everywhere
Rūdolfs Ošiņš committed 2 years ago
commit cb52e9654242d8501f5bd3081ab6799197f6781f
parent f3e5b47124e12b09cebe7e581d34472f78504a9f
24 files changed +257 -172
modified httpd-client/index.ts
@@ -12,6 +12,7 @@ import type {
  CommitHeader,
  Diff,
  DiffAddedDeletedModifiedChangeset,
+
  DiffCopiedMovedChangeset,
  HunkLine,
} from "./lib/project/commit.js";
import type { Issue, IssueState } from "./lib/project/issue.js";
@@ -39,6 +40,7 @@ export type {
  CommitHeader,
  Diff,
  DiffAddedDeletedModifiedChangeset,
+
  DiffCopiedMovedChangeset,
  DiffResponse,
  HunkLine,
  Issue,
modified src/components/Comment.svelte
@@ -1,5 +1,6 @@
<script lang="ts" strictEvents>
  import type { AuthorAliasColor } from "@app/components/Authorship.svelte";
+
  import type { BaseUrl } from "@httpd-client";

  import Authorship from "@app/components/Authorship.svelte";
  import Button from "@app/components/Button.svelte";
@@ -8,6 +9,8 @@
  import Textarea from "@app/components/Textarea.svelte";
  import { createEventDispatcher } from "svelte";

+
  export let baseUrl: BaseUrl;
+
  export let projectId: string;
  export let id: string | undefined = undefined;
  export let authorId: string;
  export let authorAlias: string | undefined = undefined;
@@ -82,7 +85,7 @@
    {:else if body.trim() === ""}
      <span class="txt-missing">No description.</span>
    {:else}
-
      <Markdown {rawPath} content={body} />
+
      <Markdown {projectId} {baseUrl} {rawPath} content={body} />
    {/if}
  </div>
</div>
modified src/components/Markdown.svelte
@@ -1,23 +1,26 @@
<script lang="ts">
+
  import type { BaseUrl } from "@httpd-client";
+

  import dompurify from "dompurify";
  import matter from "@radicle/gray-matter";
-
  import { marked } from "marked";
  import { afterUpdate } from "svelte";
+
  import { marked } from "marked";
  import { toDom } from "hast-util-to-dom";

  import * as utils from "@app/lib/utils";
-
  import { base, activeRouteStore } from "@app/lib/router";
+
  import * as router from "@app/lib/router";
  import { highlight } from "@app/lib/syntax";
  import { isUrl, twemoji, scrollIntoView, canonicalize } from "@app/lib/utils";
  import {
    markdownExtensions as extensions,
    renderer,
  } from "@app/lib/markdown";
-
  import { updateProjectRoute } from "@app/views/projects/router";

+
  export let baseUrl: BaseUrl;
  export let content: string;
  export let hash: string | undefined = undefined;
  export let path: string = "/";
+
  export let projectId: string;
  export let rawPath: string | undefined = undefined;

  $: doc = matter(content);
@@ -40,11 +43,15 @@
  function navigateToMarkdownLink(event: any) {
    if (event.target.matches(".file-link")) {
      event.preventDefault();
-
      if ($activeRouteStore.resource === "projects") {
-
        void updateProjectRoute({
+
      void router.push({
+
        resource: "projects",
+
        params: {
+
          id: projectId,
+
          baseUrl,
+
          view: { resource: "tree" },
          path: utils.canonicalize(event.target.getAttribute("href"), path),
-
        });
-
      }
+
        },
+
      });
    }
  }

@@ -68,7 +75,7 @@
        if (
          imagePath &&
          !isUrl(imagePath) &&
-
          !imagePath.startsWith(`${base}twemoji`)
+
          !imagePath.startsWith(`${router.base}twemoji`)
        ) {
          i.setAttribute("src", `${rawPath}/${canonicalize(imagePath, path)}`);
        }
deleted src/components/ProjectLink.svelte
@@ -1,31 +0,0 @@
-
<script lang="ts" strictEvents>
-
  import type { ProjectsParams } from "@app/views/projects/router";
-
  import { createEventDispatcher } from "svelte";
-

-
  import { useDefaultNavigation } from "@app/lib/router";
-
  import {
-
    projectLinkHref,
-
    updateProjectRoute,
-
  } from "@app/views/projects/router";
-

-
  export let projectParams: Partial<
-
    Omit<ProjectsParams, "id" | "route" | "hash">
-
  >;
-
  export let title: string | undefined = undefined;
-

-
  const dispatch = createEventDispatcher<{ click: null }>();
-

-
  function navigateToRoute(event: MouseEvent): void {
-
    if (useDefaultNavigation(event)) {
-
      return;
-
    }
-

-
    event.preventDefault();
-
    void updateProjectRoute(projectParams);
-
    dispatch("click");
-
  }
-
</script>
-

-
<a {title} on:click={navigateToRoute} href={projectLinkHref(projectParams)}>
-
  <slot />
-
</a>
modified src/components/Thread.svelte
@@ -1,4 +1,5 @@
<script lang="ts" strictEvents>
+
  import type { BaseUrl } from "@httpd-client";
  import type { Comment } from "@httpd-client";

  import Button from "@app/components/Button.svelte";
@@ -8,6 +9,8 @@
  import { scrollIntoView } from "@app/lib/utils";
  import { httpdStore } from "@app/lib/httpd";

+
  export let baseUrl: BaseUrl;
+
  export let projectId: string;
  export let thread: { root: Comment; replies: Comment[] };
  export let rawPath: string;
  export let showReplyTextarea = false;
@@ -73,6 +76,8 @@
<div class="comments">
  <div class="comment">
    <CommentComponent
+
      {projectId}
+
      {baseUrl}
      {rawPath}
      id={root.id}
      authorId={root.author.id}
@@ -85,6 +90,8 @@
  {#each replies as reply}
    <div class="comment reply">
      <CommentComponent
+
        {projectId}
+
        {baseUrl}
        {rawPath}
        id={reply.id}
        authorId={reply.author.id}
modified src/views/projects/Blob.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Blob } from "@httpd-client";
+
  import type { BaseUrl, Blob } from "@httpd-client";

  import { afterUpdate, onDestroy, onMount } from "svelte";
  import { toHtml } from "hast-util-to-html";
@@ -11,6 +11,8 @@
  import Readme from "@app/views/projects/Readme.svelte";
  import SquareButton from "@app/components/SquareButton.svelte";

+
  export let baseUrl: BaseUrl;
+
  export let projectId: string;
  export let path: string;
  export let hash: string | undefined = undefined;
  export let blob: Blob;
@@ -256,7 +258,13 @@
        <span class="txt-tiny">Binary content</span>
      </div>
    {:else if showMarkdown && blob.content}
-
      <Readme content={blob.content} {rawPath} {path} {hash} />
+
      <Readme
+
        {baseUrl}
+
        {projectId}
+
        content={blob.content}
+
        {rawPath}
+
        {path}
+
        {hash} />
    {:else if content}
      <table class="code no-scrollbar">
        {@html toHtml(content)}
modified src/views/projects/BranchSelector.svelte
@@ -1,12 +1,20 @@
-
<script lang="ts" strictEvents>
+
<script lang="ts">
+
  import type { BaseUrl } from "@httpd-client";
+
  import type { LoadedSourceBrowsingView } from "@app/views/projects/router";
+

  import * as utils from "@app/lib/utils";
+
  import { closeFocused } from "@app/components/Floating.svelte";

  import Dropdown from "@app/components/Dropdown.svelte";
  import DropdownItem from "@app/components/Dropdown/DropdownItem.svelte";
  import Floating from "@app/components/Floating.svelte";
-
  import ProjectLink from "@app/components/ProjectLink.svelte";
+
  import Link from "@app/components/Link.svelte";

+
  export let baseUrl: BaseUrl;
  export let branches: Record<string, string> | undefined;
+
  export let peer: string | undefined;
+
  export let projectId: string;
+
  export let view: LoadedSourceBrowsingView;
  export let selectedBranch: string | undefined;
  export let selectedCommitId: string;

@@ -72,13 +80,24 @@
        <svelte:fragment slot="modal">
          <Dropdown items={branchList}>
            <svelte:fragment slot="item" let:item>
-
              <ProjectLink projectParams={{ revision: item.value }} on:click>
+
              <Link
+
                route={{
+
                  resource: "projects",
+
                  params: {
+
                    id: projectId,
+
                    baseUrl,
+
                    peer,
+
                    revision: item.value,
+
                    view,
+
                  },
+
                }}
+
                on:afterNavigate={() => closeFocused()}>
                <DropdownItem
                  selected={item.value === selectedBranch}
                  size="tiny">
                  {item.value}
                </DropdownItem>
-
              </ProjectLink>
+
              </Link>
            </svelte:fragment>
          </Dropdown>
        </svelte:fragment>
modified src/views/projects/Browser.svelte
@@ -23,9 +23,9 @@
  export let peer: string | undefined;
  export let peers: Remote[];
  export let project: Project;
-
  export let resource: LoadedSourceBrowsingView["resource"];
  export let revision: string | undefined;
  export let tree: Tree;
+
  export let view: LoadedSourceBrowsingView;

  export let blobResult: BlobResult;

@@ -137,8 +137,8 @@
  {contributorCount}
  {peers}
  {peer}
-
  {resource}
-
  {revision} />
+
  {revision}
+
  {view} />

<main>
  <!-- Mobile navigation -->
@@ -160,12 +160,15 @@
      <div class="column-left" class:column-left-visible={mobileFileTree}>
        <div class="source-tree sticky">
          <TreeComponent
-
            {tree}
-
            {path}
-
            {fetchTree}
+
            projectId={project.id}
            revision={revision ?? project.defaultBranch}
+
            {baseUrl}
+
            {fetchTree}
+
            {path}
+
            {peer}
+
            {tree}
            on:select={() => {
-
              // Close mobile tree if user navigates to other file
+
              // Close mobile tree if user navigates to other file.
              mobileFileTree = false;
            }} />
        </div>
@@ -173,6 +176,8 @@
      <div class="column-right">
        {#if blobResult.ok}
          <BlobComponent
+
            projectId={project.id}
+
            {baseUrl}
            {path}
            {hash}
            blob={blobResult.blob}
modified src/views/projects/Cob/Revision.svelte
@@ -282,6 +282,8 @@
        {#if revisionDescription && !first}
          <div class="revision-description txt-small">
            <Markdown
+
              {baseUrl}
+
              {projectId}
              rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)}
              content={revisionDescription} />
          </div>
@@ -345,6 +347,8 @@
        <div style:margin-left="1.5rem">
          {#if element.type === "thread"}
            <Thread
+
              {baseUrl}
+
              {projectId}
              rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)}
              thread={element.inner}
              on:reply />
@@ -379,6 +383,8 @@
                class:positive-review={review.verdict === "accept"}
                class:negative-review={review.verdict === "reject"}>
                <CommentComponent
+
                  {baseUrl}
+
                  {projectId}
                  caption={formatVerdict(review.verdict)}
                  authorId={author}
                  authorAlias={review.author.alias}
modified src/views/projects/Commit.svelte
@@ -18,8 +18,8 @@
  export let peer: string | undefined = undefined;
  export let peers: Remote[];
  export let project: Project;
-
  export let resource: LoadedSourceBrowsingView["resource"];
  export let revision: string | undefined;
+
  export let view: LoadedSourceBrowsingView;

  const { commit: header } = commit;
</script>
@@ -68,8 +68,8 @@
  {contributorCount}
  {peers}
  {peer}
-
  {resource}
-
  {revision} />
+
  {revision}
+
  {view} />

<div class="commit">
  <div class="header">
@@ -89,5 +89,9 @@
    <pre class="description txt-small">{header.description}</pre>
    <CommitAuthorship {header} />
  </div>
-
  <Changeset diff={commit.diff} revision={commit.commit.id} />
+
  <Changeset
+
    projectId={project.id}
+
    {baseUrl}
+
    diff={commit.diff}
+
    revision={commit.commit.id} />
</div>
modified src/views/projects/History.svelte
@@ -20,9 +20,9 @@
  export let peer: string | undefined;
  export let peers: Remote[];
  export let project: Project;
-
  export let resource: LoadedSourceBrowsingView["resource"];
  export let revision: string | undefined;
  export let totalCommitCount: number;
+
  export let view: LoadedSourceBrowsingView;

  const api = new HttpdClient(baseUrl);

@@ -92,8 +92,8 @@
  {contributorCount}
  {peers}
  {peer}
-
  {resource}
-
  {revision} />
+
  {revision}
+
  {view} />

<div class="history">
  {#each groupCommits(allCommitHeaders) as group (group.time)}
modified src/views/projects/Issue.svelte
@@ -332,6 +332,8 @@
      </svelte:fragment>
      <div slot="description">
        <Markdown
+
          {baseUrl}
+
          {projectId}
          content={issue.discussion[0].body}
          rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)} />
      </div>
@@ -344,7 +346,12 @@
    </CobHeader>
    {#each threads as thread (thread.root.id)}
      <div class="thread">
-
        <ThreadComponent {thread} {rawPath} on:reply={createReply} />
+
        <ThreadComponent
+
          {baseUrl}
+
          {projectId}
+
          {thread}
+
          {rawPath}
+
          on:reply={createReply} />
      </div>
    {/each}
    {#if $httpdStore.state === "authenticated"}
modified src/views/projects/Issue/New.svelte
@@ -154,6 +154,8 @@
              <p class="txt-missing">No description</p>
            {:else}
              <Markdown
+
                {baseUrl}
+
                {projectId}
                content={issueText}
                rawPath={utils.getRawBasePath(
                  projectId,
modified src/views/projects/Patch.svelte
@@ -303,6 +303,8 @@
      <svelte:fragment slot="description">
        {#if patch.revisions[0].description}
          <Markdown
+
            {projectId}
+
            {baseUrl}
            content={patch.revisions[0].description}
            rawPath={utils.getRawBasePath(
              projectId,
@@ -429,7 +431,11 @@
      {#if diff}
        {#await api.project.getDiff(projectId, diff.split("..")[0], diff.split("..")[1]) then diff}
          <div style:margin-top="1rem">
-
            <Changeset revision={currentRevision.oid} diff={diff.diff} />
+
            <Changeset
+
              {projectId}
+
              {baseUrl}
+
              revision={currentRevision.oid}
+
              diff={diff.diff} />
          </div>
        {:catch e}
          <ErrorMessage
@@ -473,7 +479,11 @@
    {:else if currentTab === "files"}
      {#await api.project.getDiff(projectId, currentRevision.base, currentRevision.oid) then diff}
        <div style:margin-top="1rem">
-
          <Changeset revision={currentRevision.oid} diff={diff.diff} />
+
          <Changeset
+
            {projectId}
+
            {baseUrl}
+
            revision={currentRevision.oid}
+
            diff={diff.diff} />
        </div>
      {:catch e}
        <ErrorMessage message="Not able to load files diff." stackTrace={e} />
modified src/views/projects/PeerSelector.svelte
@@ -1,6 +1,8 @@
-
<script lang="ts" strictEvents>
-
  import type { Remote } from "@httpd-client";
+
<script lang="ts">
+
  import type { BaseUrl, Remote } from "@httpd-client";
+
  import type { LoadedSourceBrowsingView } from "@app/views/projects/router";

+
  import { closeFocused } from "@app/components/Floating.svelte";
  import { formatNodeId, truncateId } from "@app/lib/utils";
  import { pluralize } from "@app/lib/pluralize";

@@ -10,10 +12,13 @@
  import DropdownItem from "@app/components/Dropdown/DropdownItem.svelte";
  import Floating from "@app/components/Floating.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import ProjectLink from "@app/components/ProjectLink.svelte";
+
  import Link from "@app/components/Link.svelte";

+
  export let baseUrl: BaseUrl;
  export let peer: string | undefined = undefined;
  export let peers: Remote[];
+
  export let projectId: string;
+
  export let view: LoadedSourceBrowsingView;

  $: meta = peers.find(p => p.id === peer);

@@ -102,11 +107,17 @@
    <Dropdown items={peers}>
      <svelte:fragment slot="item" let:item>
        <div class="dropdown-item">
-
          <ProjectLink
-
            on:click
-
            projectParams={{
-
              peer: item.id,
-
              revision: undefined,
+
          <Link
+
            on:afterNavigate={() => closeFocused()}
+
            route={{
+
              resource: "projects",
+
              params: {
+
                id: projectId,
+
                baseUrl,
+
                peer: item.id,
+
                revision: undefined,
+
                view,
+
              },
            }}>
            <DropdownItem
              selected={item.id === peer}
@@ -133,7 +144,7 @@
                <Badge variant="primary">delegate</Badge>
              {/if}
            </DropdownItem>
-
          </ProjectLink>
+
          </Link>
        </div>
      </svelte:fragment>
    </Dropdown>
modified src/views/projects/Readme.svelte
@@ -1,10 +1,14 @@
<script lang="ts">
+
  import type { BaseUrl } from "@httpd-client";
+

  import Markdown from "@app/components/Markdown.svelte";

+
  export let baseUrl: BaseUrl;
  export let content: string;
-
  export let rawPath: string;
-
  export let path: string;
  export let hash: string | undefined = undefined;
+
  export let path: string;
+
  export let projectId: string;
+
  export let rawPath: string;
</script>

<style>
@@ -18,5 +22,5 @@
</style>

<article>
-
  <Markdown {content} {hash} {rawPath} {path} />
+
  <Markdown {baseUrl} {projectId} {content} {hash} {rawPath} {path} />
</article>
modified src/views/projects/SourceBrowser/Changeset.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Diff } from "@httpd-client";
+
  import type { BaseUrl, Diff } from "@httpd-client";

  import { pluralize } from "@app/lib/pluralize";

@@ -8,6 +8,8 @@

  export let diff: Diff;
  export let revision: string;
+
  export let baseUrl: BaseUrl;
+
  export let projectId: string;

  const diffDescription = ({
    modified,
@@ -67,18 +69,28 @@
</div>
<div class="diff-listing">
  {#each diff.added as file}
-
    <FileDiff {file} {revision} headerBadgeCaption="added" />
+
    <FileDiff
+
      {projectId}
+
      {baseUrl}
+
      {file}
+
      {revision}
+
      headerBadgeCaption="added" />
  {/each}
  {#each diff.deleted as file}
-
    <FileDiff {file} {revision} headerBadgeCaption="deleted" />
+
    <FileDiff
+
      {projectId}
+
      {baseUrl}
+
      {file}
+
      {revision}
+
      headerBadgeCaption="deleted" />
  {/each}
  {#each diff.modified as file}
-
    <FileDiff {file} {revision} />
+
    <FileDiff {projectId} {baseUrl} {file} {revision} />
  {/each}
  {#each diff.moved as file}
-
    <FileLocationChange {file} {revision} mode="moved" />
+
    <FileLocationChange {projectId} {baseUrl} {file} {revision} mode="moved" />
  {/each}
  {#each diff.copied as file}
-
    <FileLocationChange {file} {revision} mode="copied" />
+
    <FileLocationChange {projectId} {baseUrl} {file} {revision} mode="copied" />
  {/each}
</div>
modified src/views/projects/SourceBrowser/FileDiff.svelte
@@ -1,16 +1,19 @@
<script lang="ts">
  import type {
+
    BaseUrl,
    DiffAddedDeletedModifiedChangeset,
    HunkLine,
  } from "@httpd-client";

  import Badge from "@app/components/Badge.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import ProjectLink from "@app/components/ProjectLink.svelte";
+
  import Link from "@app/components/Link.svelte";

  export let file: DiffAddedDeletedModifiedChangeset;
  export let revision: string;
  export let headerBadgeCaption: "added" | "deleted" | undefined = undefined;
+
  export let baseUrl: BaseUrl;
+
  export let projectId: string;

  let collapsed = false;

@@ -170,14 +173,19 @@
      {/if}
    </div>
    <div class="browse" title="View file">
-
      <ProjectLink
-
        projectParams={{
-
          view: { resource: "tree" },
-
          path: file.path,
-
          revision,
+
      <Link
+
        route={{
+
          resource: "projects",
+
          params: {
+
            id: projectId,
+
            baseUrl,
+
            view: { resource: "tree" },
+
            path: file.path,
+
            revision,
+
          },
        }}>
        <Icon name="browse" />
-
      </ProjectLink>
+
      </Link>
    </div>
  </header>
  {#if !collapsed}
modified src/views/projects/SourceBrowser/FileLocationChange.svelte
@@ -1,13 +1,15 @@
<script lang="ts">
-
  import type { DiffCopiedMovedChangeset } from "@httpd-client/lib/project/commit";
+
  import type { BaseUrl, DiffCopiedMovedChangeset } from "@httpd-client";

  import Badge from "@app/components/Badge.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import ProjectLink from "@app/components/ProjectLink.svelte";
+
  import Link from "@app/components/Link.svelte";

  export let file: DiffCopiedMovedChangeset;
  export let revision: string;
  export let mode: "moved" | "copied";
+
  export let baseUrl: BaseUrl;
+
  export let projectId: string;
</script>

<style>
@@ -49,14 +51,19 @@
      {/if}
    </div>
    <div class="browse" title="View file">
-
      <ProjectLink
-
        projectParams={{
-
          view: { resource: "tree" },
-
          path: file.newPath,
-
          revision,
+
      <Link
+
        route={{
+
          resource: "projects",
+
          params: {
+
            id: projectId,
+
            baseUrl,
+
            view: { resource: "tree" },
+
            path: file.newPath,
+
            revision,
+
          },
        }}>
        <Icon name="browse" />
-
      </ProjectLink>
+
      </Link>
    </div>
  </header>
</div>
modified src/views/projects/SourceBrowsingHeader.svelte
@@ -2,7 +2,6 @@
  import type { BaseUrl, Remote } from "@httpd-client";
  import type { LoadedSourceBrowsingView } from "@app/views/projects/router";

-
  import { closeFocused } from "@app/components/Floating.svelte";
  import { pluralize } from "@app/lib/pluralize";

  import BranchSelector from "@app/views/projects/BranchSelector.svelte";
@@ -18,7 +17,7 @@
  export let peer: string | undefined;
  export let peers: Remote[];
  export let projectId: string;
-
  export let resource: LoadedSourceBrowsingView["resource"];
+
  export let view: LoadedSourceBrowsingView;
  export let revision: string | undefined;
  export let commitId: string;

@@ -59,14 +58,17 @@

<div class="header">
  {#if peers.length > 0}
-
    <PeerSelector {peers} {peer} on:click={() => closeFocused()} />
+
    <PeerSelector {baseUrl} {peers} {peer} {projectId} {view} />
  {/if}

  <BranchSelector
-
    {branches}
    selectedCommitId={commitId}
+
    {baseUrl}
+
    {branches}
+
    {peer}
+
    {projectId}
    {selectedBranch}
-
    on:click={() => closeFocused()} />
+
    {view} />

  <Link
    route={{
@@ -74,14 +76,13 @@
      params: {
        id: projectId,
        baseUrl,
-
        view: {
-
          resource: "history",
-
        },
        peer,
        revision,
+
        view: { resource: "history" },
      },
    }}>
-
    <SquareButton active={resource === "history" || resource === "commits"}>
+
    <SquareButton
+
      active={view.resource === "history" || view.resource === "commits"}>
      <span class="txt-bold">{commitCount}</span>
      {pluralize("commit", commitCount)}
    </SquareButton>
modified src/views/projects/Tree.svelte
@@ -1,16 +1,19 @@
<script lang="ts" strictEvents>
-
  import type { Tree } from "@httpd-client";
+
  import type { BaseUrl, Tree } from "@httpd-client";

  import { createEventDispatcher } from "svelte";

  import File from "./Tree/File.svelte";
  import Folder from "./Tree/Folder.svelte";
-
  import ProjectLink from "@app/components/ProjectLink.svelte";
+
  import Link from "@app/components/Link.svelte";

+
  export let baseUrl: BaseUrl;
  export let fetchTree: (path: string) => Promise<Tree | undefined>;
  export let path: string;
-
  export let tree: Tree;
+
  export let peer: string | undefined;
+
  export let projectId: string;
  export let revision: string;
+
  export let tree: Tree;

  const dispatch = createEventDispatcher<{ select: string }>();
  const onSelect = ({ detail: path }: { detail: string }): void => {
@@ -21,17 +24,30 @@
{#each tree.entries as entry (entry.path)}
  {#if entry.kind === "tree"}
    <Folder
-
      {fetchTree}
-
      {revision}
+
      currentPath={path}
      name={entry.name}
+
      on:select={onSelect}
      prefix={`${entry.path}/`}
-
      currentPath={path}
-
      on:select={onSelect} />
+
      {baseUrl}
+
      {fetchTree}
+
      {peer}
+
      {projectId}
+
      {revision} />
  {:else}
-
    <ProjectLink
-
      projectParams={{ view: { resource: "tree" }, path: entry.path, revision }}
-
      on:click={() => onSelect({ detail: entry.path })}>
+
    <Link
+
      route={{
+
        resource: "projects",
+
        params: {
+
          id: projectId,
+
          baseUrl,
+
          path: entry.path,
+
          peer,
+
          revision,
+
          view: { resource: "tree" },
+
        },
+
      }}
+
      on:afterNavigate={() => onSelect({ detail: entry.path })}>
      <File active={entry.path === path} name={entry.name} />
-
    </ProjectLink>
+
    </Link>
  {/if}
{/each}
modified src/views/projects/Tree/Folder.svelte
@@ -1,17 +1,20 @@
<script lang="ts" strictEvents>
-
  import type { Tree } from "@httpd-client";
+
  import type { BaseUrl, Tree } from "@httpd-client";

  import { createEventDispatcher } from "svelte";

  import Loading from "@app/components/Loading.svelte";
-
  import ProjectLink from "@app/components/ProjectLink.svelte";
+
  import Link from "@app/components/Link.svelte";

  import File from "./File.svelte";

+
  export let baseUrl: BaseUrl;
+
  export let currentPath: string;
  export let fetchTree: (path: string) => Promise<Tree | undefined>;
  export let name: string;
+
  export let peer: string | undefined;
  export let prefix: string;
-
  export let currentPath: string;
+
  export let projectId: string;
  export let revision: string;

  $: expanded = currentPath.indexOf(prefix) === 0;
@@ -80,23 +83,34 @@
      {#if tree}
        {#each tree.entries as entry (entry.path)}
          {#if entry.kind === "tree"}
+
            <!-- svelte:self doesn't check types, make sure to pass in all
+
            required props! -->
            <svelte:self
-
              {fetchTree}
              name={entry.name}
              on:select={onSelectFile}
              prefix={`${entry.path}/`}
-
              {revision}
-
              {currentPath} />
+
              {baseUrl}
+
              {currentPath}
+
              {fetchTree}
+
              {peer}
+
              {projectId}
+
              {revision} />
          {:else}
-
            <ProjectLink
-
              projectParams={{
-
                view: { resource: "tree" },
-
                path: entry.path,
-
                revision,
+
            <Link
+
              route={{
+
                resource: "projects",
+
                params: {
+
                  id: projectId,
+
                  baseUrl,
+
                  path: entry.path,
+
                  peer,
+
                  revision,
+
                  view: { resource: "tree" },
+
                },
              }}
-
              on:click={() => onSelectFile({ detail: entry.path })}>
+
              on:afterNavigate={() => onSelectFile({ detail: entry.path })}>
              <File active={entry.path === currentPath} name={entry.name} />
-
            </ProjectLink>
+
            </Link>
          {/if}
        {/each}
      {/if}
modified src/views/projects/View.svelte
@@ -67,13 +67,13 @@
      contributorCount={view.params.loadedTree.stats.contributors}
      path={view.path}
      peers={view.params.loadedPeers}
-
      resource={view.resource}
      tree={view.params.loadedTree}
      {baseUrl}
      {hash}
      {peer}
      {project}
-
      {revision} />
+
      {revision}
+
      {view} />
  {:else if view.resource === "history"}
    <History
      branches={view.params.loadedBranches}
@@ -81,12 +81,12 @@
      commitHeaders={view.commitHeaders}
      contributorCount={view.params.loadedTree.stats.contributors}
      peers={view.params.loadedPeers}
-
      resource={view.resource}
      totalCommitCount={view.totalCommitCount}
      {baseUrl}
      {peer}
      {project}
-
      {revision} />
+
      {revision}
+
      {view} />
  {:else if view.resource === "commits"}
    <Commit
      branches={view.params.loadedBranches}
@@ -94,11 +94,11 @@
      commitCount={view.params.loadedTree.stats.commits}
      contributorCount={view.params.loadedTree.stats.contributors}
      peers={view.params.loadedPeers}
-
      resource={view.resource}
      {baseUrl}
      {peer}
      {project}
-
      {revision} />
+
      {revision}
+
      {view} />
  {:else if view.resource === "issues"}
    {#if view.params.view.resource === "new"}
      <NewIssue projectId={id} projectHead={project.head} {baseUrl} />
modified src/views/projects/router.ts
@@ -11,10 +11,7 @@ import type {
  Tree,
} from "@httpd-client";

-
import { get } from "svelte/store";
-

import { HttpdClient } from "@httpd-client";
-
import { activeRouteStore, push, replace, routeToPath } from "@app/lib/router";
import * as Syntax from "@app/lib/syntax";
import { unreachable } from "@app/lib/utils";
import { seedPath } from "@app/views/seeds/router";
@@ -477,40 +474,6 @@ function createProjectRoute(
  };
}

-
export function projectLinkHref(
-
  projectRouteParams: Partial<Omit<ProjectsParams, "id" | "route" | "hash">>,
-
): string | undefined {
-
  const activeRoute = get(activeRouteStore);
-

-
  if (activeRoute.resource === "projects") {
-
    return routeToPath(createProjectRoute(activeRoute, projectRouteParams));
-
  } else {
-
    throw new Error(
-
      "Don't use project specific navigation outside of project views",
-
    );
-
  }
-
}
-

-
export async function updateProjectRoute(
-
  projectRouteParams: Partial<Omit<ProjectsParams, "id" | "route" | "hash">>,
-
  opts: { replace: boolean } = { replace: false },
-
): Promise<void> {
-
  const activeRoute = get(activeRouteStore);
-

-
  if (activeRoute.resource === "projects") {
-
    const updatedRoute = createProjectRoute(activeRoute, projectRouteParams);
-
    if (opts.replace) {
-
      await replace(updatedRoute);
-
    } else {
-
      await push(updatedRoute);
-
    }
-
  } else {
-
    throw new Error(
-
      "Don't use project specific navigation outside of project views",
-
    );
-
  }
-
}
-

export function resolveProjectRoute(
  url: URL,
  baseUrl: BaseUrl,