Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Migrate to the heartwood protocol stack
Rūdolfs Ošiņš committed 3 years ago
commit 2bad94457ca33b20b3fde998f02ba94418bbe337
parent d1168dc6ff55fd15674ac3f48c66fe2d527ee0ee
59 files changed +460 -1981
modified .github/workflows/check-e2e.yml
@@ -14,8 +14,8 @@ jobs:
      matrix:
        # Disable firefox until this is fixed upstream:
        # https://github.com/microsoft/playwright/issues/21145
-
        # browser: [chromium, firefox, visual, heartwood]
-
        browser: [chromium, visual, heartwood]
+
        # browser: [chromium, firefox, visual]
+
        browser: [chromium, visual]
    timeout-minutes: 30
    runs-on: ubuntu-latest
    steps:
@@ -59,19 +59,13 @@ jobs:

      - name: Start http-api test server
        run: |
-
          if [ ${{ matrix.browser }} = "heartwood" ]; then
-
            mkdir -p tests/artifacts
-
            ./scripts/run-httpd-with-fixtures --non-interactive --download 2>&1 | tee tests/artifacts/httpd-${{ matrix.browser }}.log &
-
          else
-
            ./scripts/run-http-api-with-fixtures --non-interactive --detach
-
          fi
+
          mkdir -p tests/artifacts;
+
          ./scripts/run-httpd-with-fixtures --non-interactive --download 2>&1 | tee tests/artifacts/httpd-${{ matrix.browser }}.log &

      - name: Run Playwright tests
        run: |
          if [ ${{ matrix.browser }} = "visual" ] && [ ${{ github.ref }} = "refs/heads/master" ]; then
            npm run test:e2e -- --project ${{ matrix.browser }} --update-snapshots;
-
          elif [ ${{ matrix.browser }} = "heartwood" ]; then
-
            HEARTWOOD=true npm run test:e2e -- --project chromium;
          else
            npm run test:e2e -- --project ${{ matrix.browser }};
          fi
modified scripts/create-seed-fixture
@@ -1,30 +1,29 @@
#!/usr/bin/env bash
-
set -euo pipefail

-
function cleanup {
-
  docker kill radicle-git-server-test
-
}
-
trap cleanup EXIT
+
killall radicle-node
+
killall git-daemon

+
set -euo pipefail

-
PASSPHRASE=asdf
-
REV=40bdc662b2d48cbfaa87182b21457ef8f861b04a
+
export RAD_PASSPHRASE=asdf

REPO_ROOT=$(git rev-parse --show-toplevel)
-
ID=$(echo $RANDOM | md5sum | head -c 8)
-
BASE_PATH=$REPO_ROOT/tests/tmp/create-seed-fixture-$ID
+
ID=$(echo $RANDOM | md5sum | head -c 4)
+
BASE_PATH=$REPO_ROOT/tests/tmp/seed-fixture-$ID

TEST_REPO_ARCHIVE=$REPO_ROOT/tests/fixtures/repos/source-browsing.tar.bz2
TEST_REPO_NAME=source-browsing
TEST_REPO_PATH=$BASE_PATH/repos/$TEST_REPO_NAME

PALM_RAD_HOME=$BASE_PATH/seeds/palm
+
PALM_CHECKOUT=$BASE_PATH/checkout/palm
ALICE_RAD_HOME=$BASE_PATH/peers/alice
ALICE_CHECKOUT=$BASE_PATH/checkout/alice
BOB_RAD_HOME=$BASE_PATH/peers/bob
BOB_CHECKOUT=$BASE_PATH/checkout/bob

mkdir -p $PALM_RAD_HOME
+
mkdir -p $PALM_CHECKOUT
mkdir -p $ALICE_RAD_HOME
mkdir -p $ALICE_CHECKOUT
mkdir -p $BOB_RAD_HOME
@@ -33,62 +32,89 @@ mkdir -p $TEST_REPO_PATH

tar -xf $TEST_REPO_ARCHIVE -C $TEST_REPO_PATH

-
RAD_HOME=$PALM_RAD_HOME rad auth --init --name palm --passphrase $PASSPHRASE
+
### PALM NODE ###
+

+
eval $(ssh-agent)
+

+
export RAD_HOME=$PALM_RAD_HOME
+
export RAD_SEED=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee
+

+
rad auth
+
radicle-node --listen 0.0.0.0:3446 --git-daemon 0.0.0.0:4446 &

-
docker run \
-
  --detach \
-
  --init \
-
  --publish 8778:8778 \
-
  --rm \
-
  --env RAD_HOME=/app/radicle \
-
  --name radicle-git-server-test \
-
  --volume $PALM_RAD_HOME:/app/radicle \
-
  "gcr.io/radicle-services/git-server:$REV" \
-
  --passphrase $PASSPHRASE \
-
  --allow-unauthorized-keys
+
### ALICE ###

-
# git-server takes a while to copy commit hooks to the monorepo
-
sleep 10
+
export RAD_HOME=$ALICE_RAD_HOME
+
export RAD_SEED=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

-
GIT_AUTHOR_NAME="Alice Liddell"
-
GIT_AUTHOR_EMAIL="alice@radicle.xyz"
-
GIT_COMMITTER_NAME="Alice Liddell"
-
GIT_COMMITTER_EMAIL="alice@radicle.xyz"
+
export GIT_AUTHOR_NAME="Alice Liddell"
+
export GIT_AUTHOR_EMAIL="alice@radicle.xyz"
+
export GIT_COMMITTER_NAME="Alice Liddell"
+
export GIT_COMMITTER_EMAIL="alice@radicle.xyz"

-
RAD_HOME=$ALICE_RAD_HOME rad auth --init --name alice --passphrase $PASSPHRASE
+
rad auth

cd $ALICE_CHECKOUT

git clone $TEST_REPO_PATH
cd $TEST_REPO_NAME
+

+
git checkout main
+
rad init --name "source-browsing" \
+
	--description "Git repository for source browsing tests" \
+
	--default-branch "main" \
+
	--no-confirm
+

git checkout feature/branch
+
git push rad
+

git checkout orphaned-branch
-
git checkout main
+
git push rad
+

+
radicle-node --listen 0.0.0.0:3444 --git-daemon 0.0.0.0:4444 \
+
	--connect z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8@0.0.0.0:3446 &
+

+
sleep 1
+

+
### PALM CLONE ###

-
RAD_HOME=$ALICE_RAD_HOME rad init --name "source-browsing" --description "Git repository for source browsing tests" --default-branch "main" --no-confirm
+
cd $PALM_CHECKOUT
+
export RAD_HOME=$PALM_RAD_HOME
+
rad clone rad:zKtT7DmF9H34KkvcKj9PHW19WzjT

-
sleep 10
-
RAD_HOME=$ALICE_RAD_HOME rad push --seed 0.0.0.0:8778 --all --sync
-
PROJECT_ID=$(rad .)
+
### BOB ###

+
export GIT_AUTHOR_NAME="Bob Belcher"
+
export GIT_AUTHOR_EMAIL="bob@radicle.xyz"
+
export GIT_COMMITTER_NAME="Bob Belcher"
+
export GIT_COMMITTER_EMAIL="bob@radicle.xyz"
+
export GIT_COMMITTER_DATE="Mon Dec 21 14:00 2022 +0100"

-
GIT_AUTHOR_NAME="Bob Belcher"
-
GIT_AUTHOR_EMAIL="bob@radicle.xyz"
-
GIT_COMMITTER_NAME="Bob Belcher"
-
GIT_COMMITTER_EMAIL="bob@radicle.xyz"
+
export RAD_HOME=$BOB_RAD_HOME
+
export RAD_SEED=fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe

-
RAD_HOME=$BOB_RAD_HOME rad auth --init --name bob --passphrase $PASSPHRASE
+
rad auth
+
rad auth
+
radicle-node --listen 0.0.0.0:3445 --git-daemon 0.0.0.0:4445 \
+
	--connect z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8@0.0.0.0:3446 &
+

+
sleep 2

cd $BOB_CHECKOUT
-
RAD_HOME=$BOB_RAD_HOME rad clone $PROJECT_ID --seed 0.0.0.0:8778 --no-confirm
+
rad clone rad:zKtT7DmF9H34KkvcKj9PHW19WzjT

cd $TEST_REPO_NAME
echo "Updated readme" > README.md
git add README.md
-
git commit --message "Update readme" --date "Mon Nov 21 14:00 2022 +0100"
-
RAD_HOME=$BOB_RAD_HOME rad push --seed 0.0.0.0:8778
-
RAD_HOME=$BOB_RAD_HOME rad sync --seed 0.0.0.0:8778 --self
-
RAD_HOME=$BOB_RAD_HOME rad sync --seed 0.0.0.0:8778
+
git commit --message "Update readme" --date "$GIT_COMMITTER_DATE"
+
git push rad
+

+
### WAIT FOR SYNC WITH PALM ###
+

+
sleep 2

cd $BASE_PATH
tar -cjf palm.tar.bz2 --exclude "post-receive" --exclude "pre-receive" -C $PALM_RAD_HOME .
+

+
killall radicle-node
+
killall git-daemon
deleted scripts/create-seed-fixture-heartwood
@@ -1,120 +0,0 @@
-
#!/usr/bin/env bash
-

-
killall radicle-node
-
killall git-daemon
-

-
set -euo pipefail
-

-
export RAD_PASSPHRASE=asdf
-

-
REPO_ROOT=$(git rev-parse --show-toplevel)
-
ID=$(echo $RANDOM | md5sum | head -c 4)
-
BASE_PATH=$REPO_ROOT/tests/tmp/heartwood-$ID
-

-
TEST_REPO_ARCHIVE=$REPO_ROOT/tests/fixtures/repos/source-browsing.tar.bz2
-
TEST_REPO_NAME=source-browsing
-
TEST_REPO_PATH=$BASE_PATH/repos/$TEST_REPO_NAME
-

-
PALM_RAD_HOME=$BASE_PATH/seeds/palm
-
PALM_CHECKOUT=$BASE_PATH/checkout/palm
-
ALICE_RAD_HOME=$BASE_PATH/peers/alice
-
ALICE_CHECKOUT=$BASE_PATH/checkout/alice
-
BOB_RAD_HOME=$BASE_PATH/peers/bob
-
BOB_CHECKOUT=$BASE_PATH/checkout/bob
-

-
mkdir -p $PALM_RAD_HOME
-
mkdir -p $PALM_CHECKOUT
-
mkdir -p $ALICE_RAD_HOME
-
mkdir -p $ALICE_CHECKOUT
-
mkdir -p $BOB_RAD_HOME
-
mkdir -p $BOB_CHECKOUT
-
mkdir -p $TEST_REPO_PATH
-

-
tar -xf $TEST_REPO_ARCHIVE -C $TEST_REPO_PATH
-

-
### PALM NODE ###
-

-
eval $(ssh-agent)
-

-
export RAD_HOME=$PALM_RAD_HOME
-
export RAD_SEED=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee
-

-
rad auth
-
radicle-node --listen 0.0.0.0:3446 --git-daemon 0.0.0.0:4446 &
-

-
### ALICE ###
-

-
export RAD_HOME=$ALICE_RAD_HOME
-
export RAD_SEED=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
-

-
export GIT_AUTHOR_NAME="Alice Liddell"
-
export GIT_AUTHOR_EMAIL="alice@radicle.xyz"
-
export GIT_COMMITTER_NAME="Alice Liddell"
-
export GIT_COMMITTER_EMAIL="alice@radicle.xyz"
-

-
rad auth
-

-
cd $ALICE_CHECKOUT
-

-
git clone $TEST_REPO_PATH
-
cd $TEST_REPO_NAME
-

-
git checkout main
-
rad init --name "source-browsing" \
-
	--description "Git repository for source browsing tests" \
-
	--default-branch "main" \
-
	--no-confirm
-

-
git checkout feature/branch
-
git push rad
-

-
git checkout orphaned-branch
-
git push rad
-

-
radicle-node --listen 0.0.0.0:3444 --git-daemon 0.0.0.0:4444 \
-
	--connect z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8@0.0.0.0:3446 &
-

-
sleep 1
-

-
### PALM CLONE ###
-

-
cd $PALM_CHECKOUT
-
export RAD_HOME=$PALM_RAD_HOME
-
rad clone rad:zKtT7DmF9H34KkvcKj9PHW19WzjT
-

-
### BOB ###
-

-
export GIT_AUTHOR_NAME="Bob Belcher"
-
export GIT_AUTHOR_EMAIL="bob@radicle.xyz"
-
export GIT_COMMITTER_NAME="Bob Belcher"
-
export GIT_COMMITTER_EMAIL="bob@radicle.xyz"
-
export GIT_COMMITTER_DATE="Mon Dec 21 14:00 2022 +0100"
-

-
export RAD_HOME=$BOB_RAD_HOME
-
export RAD_SEED=fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe
-

-
rad auth
-
rad auth
-
radicle-node --listen 0.0.0.0:3445 --git-daemon 0.0.0.0:4445 \
-
	--connect z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8@0.0.0.0:3446 &
-

-
sleep 2
-

-
cd $BOB_CHECKOUT
-
rad clone rad:zKtT7DmF9H34KkvcKj9PHW19WzjT
-

-
cd $TEST_REPO_NAME
-
echo "Updated readme" > README.md
-
git add README.md
-
git commit --message "Update readme" --date "$GIT_COMMITTER_DATE"
-
git push rad
-

-
### WAIT FOR SYNC WITH PALM ###
-

-
sleep 2
-

-
cd $BASE_PATH
-
tar -cjf palm.tar.bz2 --exclude "post-receive" --exclude "pre-receive" -C $PALM_RAD_HOME .
-

-
killall radicle-node
-
killall git-daemon
deleted scripts/run-http-api-with-fixtures
@@ -1,128 +0,0 @@
-
#!/bin/sh
-
set -e
-

-
REV=40bdc662b2d48cbfaa87182b21457ef8f861b04a
-

-
REPO_ROOT=$(git rev-parse --show-toplevel)
-
FIXTURE=$REPO_ROOT/tests/fixtures/seeds/palm.tar.bz2
-
WORKSPACE=$REPO_ROOT/tests/tmp/palm
-
PASSPHRASE=asdf
-
CONTAINER_NAME=radicle-http-api-with-fixtures
-
HTTP_API_BINARY=radicle-http-api
-

-
show_usage() {
-
  echo
-
  echo "Starts a http-api backend with test fixtures."
-
  echo
-
  echo "USAGE:"
-
  echo "  run-http-api-with-fixtures [-b|d|h|n]"
-
  echo
-
  echo "OPTIONS:"
-
  echo "  -b --binary            Use a ${HTTP_API_BINARY} binary that is in PATH to avoid using Docker."
-
  echo "  -d --detach            Daemonize the docker process."
-
  echo "  -h --help              Print this Help."
-
  echo "  -n --non-interactive   Run in non-interactive mode, no user prompts."
-
  echo
-
}
-

-
prompt_workspace_removal() {
-
  echo "This will irrevocably destroy the following directories:"
-
  echo
-
  echo $WORKSPACE
-
  echo
-

-
  read -r -p "Are you sure you want to continue? [yes/no]: " confirm
-
  case "$confirm" in
-
    [yY][eE][sS])
-
      rm -rf $WORKSPACE
-
      echo "Done"
-
      ;;
-
    *)
-
      echo "Ok, I won't touch your data."
-
      exit
-
      ;;
-
  esac
-
}
-

-
prepare_workspace() {
-
  echo
-
  echo "Unpacking fixture $FIXTURE"
-
  mkdir -p $WORKSPACE
-
  tar -xf $FIXTURE -C $WORKSPACE
-
}
-

-
run_docker() {
-
  echo "Starting docker at container $CONTAINER_NAME at $REV"
-
  echo "  http-api --root $WORKSPACE --passphrase $PASSPHRASE"
-
  echo
-

-
  exec docker run \
-
    --init \
-
    --publish 8777:8777 \
-
    --rm \
-
    --name $CONTAINER_NAME \
-
    --volume $WORKSPACE:/app/radicle \
-
    "$@" \
-
    "gcr.io/radicle-services/http-api:$REV" \
-
    --passphrase $PASSPHRASE
-
}
-

-
run_binary() {
-
  if ! [ -x "$(command -v $HTTP_API_BINARY)" ]; then
-
    echo
-
    echo "Couldn't find the $HTTP_API_BINARY binary in your PATH."
-
    echo "You can compile it from source:"
-
    echo "  👉 https://github.com/radicle-dev/radicle-client-services/tree/${REV}/http-api"
-
    echo
-
    exit 1
-
  fi
-

-
  echo
-
  echo "Starting $HTTP_API_BINARY"
-
  echo "  $HTTP_API_BINARY --listen 0.0.0.0:8777 --root ${WORKSPACE} --passphrase $PASSPHRASE"
-
  echo
-

-
  $HTTP_API_BINARY --listen 0.0.0.0:8777 --root $WORKSPACE --passphrase $PASSPHRASE
-
}
-

-
BINARY=false
-
NON_INTERACTIVE=false
-
DETACH=false
-

-
while [ $# -ne 0 ]; do
-
  case "$1" in
-
    --binary | -b)
-
      BINARY=true
-
      ;;
-
    --detach | -d)
-
      DETACH=true
-
      ;;
-
    --non-interactive | -n)
-
      NON_INTERACTIVE=true
-
      ;;
-
    *)
-
      show_usage
-
      exit
-
      ;;
-
  esac
-

-
  shift
-
done
-

-
if [ "$NON_INTERACTIVE" = true ]; then
-
  rm -rf $WORKSPACE
-
  prepare_workspace
-
else
-
  prompt_workspace_removal
-
  prepare_workspace
-
fi
-

-
if [ "$BINARY" = true ]; then
-
  run_binary
-
else
-
  if [ "$DETACH" = true ]; then
-
    run_docker --detach
-
  else
-
    run_docker
-
  fi
-
fi
modified scripts/run-httpd-with-fixtures
@@ -4,7 +4,7 @@ set -e
REV=f1de61ad8897a845f2be49e3e2b2951bc678b678

REPO_ROOT=$(git rev-parse --show-toplevel)
-
FIXTURE=$REPO_ROOT/tests/fixtures/seeds/palm-heartwood.tar.bz2
+
FIXTURE=$REPO_ROOT/tests/fixtures/seeds/palm.tar.bz2
WORKSPACE=$REPO_ROOT/tests/tmp/palm
PASSPHRASE=asdf
BINARY_PATH="$REPO_ROOT/tests/tmp"
modified src/App.svelte
@@ -13,7 +13,7 @@
  import Home from "@app/views/home/Index.svelte";
  import Session from "@app/views/session/Index.svelte";
  import Projects from "@app/views/projects/View.svelte";
-
  import Seeds from "@app/views/seeds/Routes.svelte";
+
  import Seeds from "@app/views/seeds/View.svelte";

  const activeRouteStore = router.activeRouteStore;

@@ -60,7 +60,7 @@
    {#if $activeRouteStore.resource === "home"}
      <Home />
    {:else if $activeRouteStore.resource === "seeds"}
-
      <Seeds host={$activeRouteStore.params.host} />
+
      <Seeds hostAndPort={$activeRouteStore.params.host} />
    {:else if $activeRouteStore.resource === "session"}
      <Session activeRoute={$activeRouteStore} />
    {:else if $activeRouteStore.resource === "projects"}
modified src/App/Header/Connect.svelte
@@ -3,17 +3,13 @@

  import { closeFocused } from "@app/components/Floating.svelte";
  import { sessionStore, disconnect } from "@app/lib/session";
-
  import { toClipboard } from "@app/lib/utils";
+
  import { toClipboard, formatNodeId } from "@app/lib/utils";

  import Avatar from "@app/components/Comment/Avatar.svelte";
  import Button from "@app/components/Button.svelte";
  import Floating from "@app/components/Floating.svelte";
  import Icon from "@app/components/Icon.svelte";

-
  function formatId(id: string) {
-
    return id.substring(0, 4) + " – " + id.substring(id.length - 4, id.length);
-
  }
-

  let icon: "clipboard-small" | "checkmark-small" = "clipboard-small";

  const restoreIcon = debounce(() => {
@@ -43,7 +39,7 @@
    position: absolute;
    right: 5rem;
    top: 5rem;
-
    width: 16.5rem;
+
    width: 15rem;
  }
  .info {
    align-items: flex-start;
@@ -118,7 +114,6 @@
    font-family: var(--font-family-monospace);
    font-size: var(--font-size-tiny);
    user-select: none;
-
    width: 11rem;
    word-break: break-all;
  }
  .disconnect {
@@ -171,7 +166,7 @@
              title={$sessionStore.publicKey} />
          </div>
          <div class="user-id txt-small">
-
            {formatId($sessionStore.publicKey)}
+
            {formatNodeId($sessionStore.publicKey)}
          </div>
        </div>
      {:else}
@@ -214,7 +209,7 @@
              }
            }}>
            <div class="id">
-
              {$sessionStore.publicKey}
+
              {formatNodeId($sessionStore.publicKey)}
            </div>
            <div class="id-clipboard">
              <Icon name={icon} />
modified src/App/Header/SearchResultsModal.svelte
@@ -4,7 +4,7 @@
  import * as modal from "@app/lib/modal";
  import Link from "@app/components/Link.svelte";
  import Modal from "@app/components/Modal.svelte";
-
  import { formatRadicleId, getSeedEmoji } from "@app/lib/utils";
+
  import { formatRepositoryId, getSeedEmoji } from "@app/lib/utils";

  export let query: string;
  export let results: ProjectResult[];
@@ -48,7 +48,7 @@
                  {getSeedEmoji(project.seed.host)}&nbsp;{project.info.name}
                </span>
                <span class="id">
-
                  &nbsp;{formatRadicleId(project.info.id)}
+
                  &nbsp;{formatRepositoryId(project.info.id)}
                </span>
              </span>
            </Link>
modified src/components/Authorship.svelte
@@ -1,11 +1,7 @@
<script lang="ts">
  import type { Author } from "@app/lib/cobs";

-
  import {
-
    formatNodeId,
-
    formatRadicleId,
-
    formatTimestamp,
-
  } from "@app/lib/utils";
+
  import { formatNodeId, formatTimestamp } from "@app/lib/utils";

  export let author: Author;
  export let timestamp: number;
@@ -37,13 +33,9 @@
</style>

<span class="authorship txt-tiny">
-
  <span class="id highlight layout-desktop">
-
    {window.HEARTWOOD ? formatNodeId(author.id) : formatRadicleId(author.id)}
-
  </span>
+
  <span class="id highlight layout-desktop">{formatNodeId(author.id)}</span>
  <span class="id highlight layout-mobile">
-
    {window.HEARTWOOD
-
      ? formatNodeId(author.id).replace("did:key:", "")
-
      : formatRadicleId(author.id)}
+
    {formatNodeId(author.id).replace("did:key:", "")}
  </span>
  <span class="caption">&nbsp;{caption}&nbsp;</span>
  <span class="date">
modified src/components/Comment/Avatar.svelte
@@ -1,11 +1,10 @@
<script lang="ts">
  import { createIcon } from "@app/lib/blockies";
-
  import { isPeerId, isRadicleId } from "@app/lib/utils";
+
  import { isNodeId } from "@app/lib/utils";

  export let title: string;
  export let source: string;
  export let inline = false;
-
  export let grayscale = false;

  function handleMissingFile() {
    console.warn("Not able to locate", source);
@@ -23,10 +22,9 @@
    return avatar.toDataURL();
  }

-
  if (isRadicleId(source) || isPeerId(source)) {
+
  if (isNodeId(source)) {
    source = createContainer(source);
  }
-
  grayscale = isPeerId(title) || isRadicleId(title);
</script>

<style>
@@ -41,9 +39,6 @@
    background-size: cover;
    background-repeat: no-repeat;
  }
-
  .grayscale {
-
    filter: grayscale();
-
  }
  .inline {
    display: inline-block !important;
    width: 1rem;
@@ -58,5 +53,4 @@
  class="avatar"
  alt="avatar"
  on:error={handleMissingFile}
-
  class:inline
-
  class:grayscale />
+
  class:inline />
modified src/config.json
@@ -1,56 +1,37 @@
{
  "reactions": ["👍", "👎", "😄", "🎉", "🙁", "🚀", "👀"],
  "seeds": {
+
    "defaultHttpdPort": 443,
+
    "defaultHttpdScheme": "https",
+
    "defaultNodePort": 8776,
    "pinned": [
      {
-
        "host": "willow.radicle.garden",
-
        "emoji": "🪵"
-
      },
-
      { "host": "pine.radicle.garden", "emoji": "🌲" },
-
      { "host": "maple.radicle.garden", "emoji": "🍁" }
+
        "host": "seed.radicle.xyz",
+
        "emoji": "🌱"
+
      }
    ]
  },
  "projects": {
    "pinned": [
      {
-
        "name": "solmate",
-
        "id": "rad:git:hnrkbgczcfh9ycjtdfynmqyg478bqrso1hnty",
-
        "seed": "willow.radicle.garden"
-
      },
-
      {
-
        "name": "openzeppelin-contracts",
-
        "id": "rad:git:hnrkbx9xzjg8tf9hj9uh5qdxwuyjddsxyaw6o",
-
        "seed": "willow.radicle.garden"
-
      },
-
      {
-
        "name": "haskell-language-server",
-
        "id": "rad:git:hnrkf1jaje1e93rua64zgphg7zb14qo9zexgy",
-
        "seed": "pine.radicle.garden"
-
      },
-
      {
-
        "name": "rx",
-
        "id": "rad:git:hnrk8aoks6w3tdbcwt9dqnor9aqy8tjha3k5o",
-
        "seed": "maple.radicle.garden"
-
      },
-
      {
-
        "name": "astrolabe",
-
        "id": "rad:git:hnrkrwewct7pb1biwmf59qa3rzod8mauundzy",
-
        "seed": "willow.radicle.garden"
+
        "name": "radicle-interface",
+
        "id": "rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5",
+
        "seed": "seed.radicle.xyz"
      },
      {
-
        "name": "svelte-dappkit",
-
        "id": "rad:git:hnrkgcm1e588ewy7iiq4abrrgtf5ioq577eno",
-
        "seed": "maple.radicle.garden"
+
        "name": "heartwood",
+
        "id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
+
        "seed": "seed.radicle.xyz"
      },
      {
-
        "name": "tmyxer",
-
        "id": "rad:git:hnrkp5jgfxwxrffjsp148fyzdc457pa9ja38y",
-
        "seed": "pine.radicle.garden"
+
        "name": "rips",
+
        "id": "rad:z3trNYnLWS11cJWC6BbxDs5niGo82",
+
        "seed": "seed.radicle.xyz"
      },
      {
-
        "name": "hexade",
-
        "id": "rad:git:hnrkxj511yr48oo3usrt4jzfoucj88zphpryo",
-
        "seed": "pine.radicle.garden"
+
        "name": "radicle-git",
+
        "id": "rad:zMKWcgFLjUSVBBVmxbcMzzokRxub",
+
        "seed": "seed.radicle.xyz"
      }
    ]
  }
modified src/global.d.ts
@@ -10,8 +10,6 @@ declare global {
    PLAYWRIGHT: boolean;
    HASH_ROUTING: boolean;

-
    HEARTWOOD: boolean;
-

    // APP_CONFIG is set from within Playwright tests at runtime.
    // To better understand how it works together, have a look at:
    //   tests/support/fixtures.ts
modified src/lib/api.ts
@@ -1,26 +1,18 @@
-
import { defaultSeedPort } from "@app/lib/seed";
-
import { isLocal } from "@app/lib/utils";
-

export interface Host {
  host: string;
-
  port: number | null;
+
  port: number;
+
  scheme: string;
}

export class Request {
  path: string;
  base: string;
-
  protocol: string;
  port: number;

  constructor(path: string, api: Host) {
-
    this.port = api.port || defaultSeedPort;
-
    if (window.HEARTWOOD) {
-
      this.base = api.host + "/api";
-
    } else {
-
      this.base = api.host;
-
    }
+
    this.port = api.port;
+
    this.base = `${api.scheme}://${api.host}/api`;
    this.path = path.startsWith("/") ? path.slice(1) : path;
-
    this.protocol = isLocal(api.host) ? "http://" : "https://";
  }

  async get(
@@ -97,9 +89,7 @@ export class Request {

  // Creates a URL with an eventual query string and port.
  private createUrl(search?: string): string {
-
    const baseUrl = this.path
-
      ? `${this.protocol}${this.base}/v1/${this.path}`
-
      : `${this.protocol}${this.base}`;
+
    const baseUrl = this.path ? `${this.base}/v1/${this.path}` : this.base;

    const url = new URL(search ? `${baseUrl}?${search}` : baseUrl);
    url.port = String(this.port);
modified src/lib/cobs.ts
@@ -5,7 +5,6 @@ export interface Comment<R = null> {
  body: string;
  reactions: Record<string, number>;
  timestamp: number;
-
  replies: R; // TODO: Remove for Heartwood migration
  replyTo: R;
}

modified src/lib/config.ts
@@ -3,6 +3,9 @@ import configJson from "@app/config.json";
export interface Config {
  reactions: string[];
  seeds: {
+
    defaultHttpdPort: number;
+
    defaultNodePort: number;
+
    defaultHttpdScheme: string;
    pinned: { host: string; emoji: string }[];
  };
  projects: {
@@ -19,6 +22,9 @@ function getConfig(): Config {
    return {
      reactions: [],
      seeds: {
+
        defaultHttpdPort: 8080,
+
        defaultHttpdScheme: "http",
+
        defaultNodePort: 8776,
        pinned: [],
      },
      projects: { pinned: [] },
modified src/lib/issue.ts
@@ -14,7 +14,6 @@ export interface IIssue {
  author: Author;
  title: string;
  state: State;
-
  comment: Comment; // TODO: Remove this after we have migrated to Heartwood.
  discussion: Thread[];
  tags: string[];
  assignees: string[];
@@ -35,7 +34,6 @@ export interface Comment<R = null> {
  body: string;
  reactions: Record<string, number>;
  timestamp: number;
-
  replies: R; // TODO: Remove this after we have migrated to Heartwood.
  replyTo: R;
}

@@ -59,7 +57,6 @@ export class Issue {
  author: Author;
  title: string;
  state: State;
-
  comment: Comment; // TODO: Remove this after we have migrated to Heartwood.
  discussion: Thread[];
  tags: string[];
  assignees: string[];
@@ -70,29 +67,17 @@ export class Issue {
    this.author = issue.author;
    this.title = issue.title;
    this.state = issue.state;
-
    this.comment = issue.comment; // TODO: Remove this after we have migrated to Heartwood.
    this.discussion = issue.discussion;
    this.tags = issue.tags;
    this.assignees = issue.assignees;
-
    if (window.HEARTWOOD) {
-
      this.timestamp = issue.discussion[0].timestamp;
-
    } else {
-
      this.timestamp = issue.timestamp;
-
    }
+
    this.timestamp = issue.discussion[0].timestamp;
  }

  // Counts the amount of comments and replies in a discussion
  countComments(): number {
-
    if (window.HEARTWOOD) {
-
      return this.discussion.reduce(acc => {
-
        return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop.
-
      }, 0);
-
    } else {
-
      return this.discussion.reduce((acc, comment) => {
-
        if (comment.replies) return acc + comment.replies.length + 1; // We add all replies and 1 forathe comment in this loop.
-
        return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop.
-
      }, 0);
-
    }
+
    return this.discussion.reduce(acc => {
+
      return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop.
+
    }, 0);
  }

  static async createIssue(
deleted src/lib/patch.ts
@@ -1,251 +0,0 @@
-
import type { Author, PeerInfo } from "@app/lib/cobs";
-
import type { Comment, Thread } from "@app/lib/issue";
-
import type { Commit } from "@app/lib/commit";
-
import type { Diff, DiffStats } from "@app/lib/diff";
-
import type { Host } from "@app/lib/api";
-
import type { PeerId, Id } from "@app/lib/project";
-

-
import { Request } from "@app/lib/api";
-

-
export interface IPatch {
-
  id: string;
-
  author: Author;
-
  title: string;
-
  state: string;
-
  target: string;
-
  labels: string[];
-
  revisions: Revision[];
-
  timestamp: number;
-
}
-

-
export enum PatchTab {
-
  Timeline = "timeline",
-
  Diff = "diff",
-
}
-

-
export interface Revision {
-
  id: string;
-
  peer: PeerId;
-
  base: string;
-
  oid: string;
-
  comment: Comment;
-
  discussion: Thread[];
-
  reviews: Record<Id, Review>;
-
  merges: Merge[];
-
  changeset: {
-
    diff: Diff;
-
    commits: Commit[];
-
    stats: DiffStats;
-
  } | null;
-
  timestamp: number;
-
}
-

-
export interface Review {
-
  author: Author;
-
  verdict: Verdict | null;
-
  comment: Thread;
-
  inline: CodeComment[];
-
  timestamp: number;
-
}
-

-
export type Verdict = "accept" | "reject";
-

-
export interface CodeComment {
-
  location: CodeLocation;
-
  comment: Comment;
-
}
-

-
export interface CodeLocation {
-
  lines: number;
-
  commit: string;
-
  blob: string;
-
}
-

-
export interface Merge {
-
  peer: PeerInfo;
-
  commit: string;
-
  timestamp: number;
-
}
-

-
export function groupPatches(patches: Patch[]) {
-
  return patches.reduce(
-
    (acc: { [state: string]: Patch[] }, patch) => {
-
      acc[patch.state].push(patch);
-
      return acc;
-
    },
-
    { proposed: [] as Patch[], draft: [] as Patch[], archived: [] as Patch[] },
-
  );
-
}
-

-
export class Patch implements IPatch {
-
  id: string;
-
  author: Author;
-
  title: string;
-
  state: string;
-
  target: string;
-
  labels: string[];
-
  revisions: Revision[];
-
  timestamp: number;
-

-
  constructor(patch: IPatch) {
-
    this.id = patch.id;
-
    this.author = patch.author;
-
    this.title = patch.title;
-
    this.state = patch.state;
-
    this.target = patch.target;
-
    this.labels = patch.labels;
-
    this.revisions = patch.revisions;
-
    this.timestamp = patch.timestamp;
-
  }
-

-
  // Counts the amount of comments and replies in a discussion
-
  countComments(rev: number): number {
-
    if (window.HEARTWOOD) {
-
      return this.revisions[rev].discussion.reduce(acc => {
-
        return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop.
-
      }, 0);
-
    } else {
-
      return this.revisions[rev].discussion.reduce((acc, comment) => {
-
        if (comment.replies) return acc + comment.replies.length + 1; // We add all replies and 1 for each comment in this loop.
-
        return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop.
-
      }, 0);
-
    }
-
  }
-

-
  createTimeline(rev: number) {
-
    const timeline: TimelineElement[] = [];
-
    const comment: TimelineElement = {
-
      type: TimelineType.Comment,
-
      timestamp: this.revisions[rev].comment.timestamp,
-
      inner: this.revisions[rev].comment,
-
    };
-
    const discussions = this.revisions[rev].discussion.map(
-
      (comment): TimelineElement => {
-
        return {
-
          type: TimelineType.Thread,
-
          timestamp: comment.timestamp,
-
          inner: comment,
-
        };
-
      },
-
    );
-
    const reviews = Object.entries(this.revisions[rev].reviews).map(
-
      ([, review]): TimelineElement => {
-
        return {
-
          type: TimelineType.Review,
-
          timestamp: review.timestamp,
-
          inner: review,
-
        };
-
      },
-
    );
-
    const merges = this.revisions[rev].merges.map((merge): TimelineElement => {
-
      return {
-
        type: TimelineType.Merge,
-
        timestamp: merge.timestamp,
-
        inner: merge,
-
      };
-
    });
-
    timeline.push(comment, ...discussions, ...merges, ...reviews);
-
    return timeline.sort((a, b) => a.timestamp - b.timestamp);
-
  }
-

-
  static async getPatches(id: string, host: Host): Promise<Patch[]> {
-
    const response = await new Request(`projects/${id}/patches`, host).get();
-
    for (const patch of response) {
-
      patch.author["id"] = patch.author["urn"];
-
      delete patch.author["urn"];
-
    }
-

-
    return response.map((patch: IPatch) => new Patch(patch));
-
  }
-

-
  static async getPatch(id: string, patch: string, host: Host): Promise<Patch> {
-
    const response = await new Request(
-
      `projects/${id}/patches/${patch}`,
-
      host,
-
    ).get();
-

-
    response.author["id"] = response.author["urn"];
-
    delete response.author["urn"];
-

-
    for (const revision of response.revisions) {
-
      if (revision.changeset?.diff) {
-
        revision.changeset.diff["added"] = revision.changeset.diff["created"];
-
        delete revision.changeset.diff["created"];
-

-
        revision.comment.author["id"] = revision.comment.author["urn"];
-
        delete revision.comment.author["urn"];
-

-
        revision.changeset.stats["insertions"] =
-
          revision.changeset.stats["additions"];
-
        delete revision.changeset.stats["additions"];
-

-
        for (const kind of ["added", "deleted", "modified"]) {
-
          for (const file of revision.changeset.diff[kind]) {
-
            for (const hunk of file.diff.hunks) {
-
              for (const line of hunk.lines) {
-
                if (line["lineNumOld"]) {
-
                  line["lineNoOld"] = line["lineNumOld"];
-
                  delete line["lineNumOld"];
-
                }
-

-
                if (line["lineNumNew"]) {
-
                  line["lineNoNew"] = line["lineNumNew"];
-
                  delete line["lineNumNew"];
-
                }
-

-
                if (line["lineNum"]) {
-
                  line["lineNo"] = line["lineNum"];
-
                  delete line["lineNum"];
-
                }
-
              }
-
            }
-
          }
-
        }
-
      }
-
    }
-

-
    return new Patch(response);
-
  }
-
}
-

-
export const formatVerdict = (verdict: string | null): string => {
-
  switch (verdict) {
-
    case "accept":
-
      return "approved this revision";
-

-
    case "reject":
-
      return "rejected this revision";
-

-
    default:
-
      return "reviewed and left a comment";
-
  }
-
};
-

-
export enum TimelineType {
-
  Comment,
-
  Thread,
-
  Review,
-
  Merge,
-
}
-

-
export type TimelineElement =
-
  | {
-
      type: TimelineType.Thread;
-
      inner: Thread;
-
      timestamp: number;
-
    }
-
  | {
-
      type: TimelineType.Comment;
-
      inner: Comment;
-
      timestamp: number;
-
    }
-
  | {
-
      type: TimelineType.Merge;
-
      inner: Merge;
-
      timestamp: number;
-
    }
-
  | {
-
      type: TimelineType.Review;
-
      inner: Review;
-
      timestamp: number;
-
    };
modified src/lib/project.ts
@@ -2,28 +2,15 @@ import type { Commit, CommitHeader, CommitsHistory } from "@app/lib/commit";
import type { Host } from "@app/lib/api";
import type { ProjectResult } from "@app/lib/search";

+
import * as utils from "@app/lib/utils";
import { Request } from "@app/lib/api";
-
import { Seed, defaultSeedPort } from "@app/lib/seed";
-
import { isFulfilled, isOid, isRadicleId } from "@app/lib/utils";
+
import { Seed } from "@app/lib/seed";
+
import { isFulfilled, isOid, isRepositoryId } from "@app/lib/utils";

-
export type Id = string;
-
export type PeerId = string;
export type Branches = { [key: string]: string };
export type MaybeBlob = Blob | undefined;
export type MaybeTree = Tree | undefined;

-
// TODO: Remove this after we have migrated to Heartwood.
-
export type Delegate =
-
  | {
-
      type: "indirect";
-
      id: Id;
-
      ids: PeerId[];
-
    }
-
  | {
-
      type: "direct";
-
      id: PeerId;
-
    };
-

// Enumerates the space below the Header component in the projects View component
export enum ProjectContent {
  Tree,
@@ -31,8 +18,6 @@ export enum ProjectContent {
  Commit,
  Issues,
  Issue,
-
  Patches,
-
  Patch,
}

export interface ProjectInfo {
@@ -42,7 +27,6 @@ export interface ProjectInfo {
  description: string;
  defaultBranch: string;
  delegates: string[];
-
  remotes: PeerId[]; // TODO: Remove this after we have migrated to Heartwood.
  patches: {
    proposed: number;
    draft: number;
@@ -56,45 +40,30 @@ export interface ProjectInfo {

export interface Tree {
  path: string;
-
  info: EntryInfo; // TODO: Remove this after we have migrated to Heartwood.
  entries: Array<Entry>;
  stats: Stats;
  name: string;
-
  kind: Kind;
  lastCommit: CommitHeader;
}

-
// TODO: Remove "TREE" and "BLOB" after we have migrated to Heartwood.
-
type Kind = "tree" | "blob" | "TREE" | "BLOB";
+
type Kind = "tree" | "blob";

export interface Stats {
  commits: number;
  contributors: number;
}

-
// TODO: Remove this after we have migrated to Heartwood.
-
export interface EntryInfo {
-
  name: string;
-
  objectType: Kind;
-
  lastCommit: CommitHeader;
-
}
-

export interface Entry {
  path: string;
-
  info: EntryInfo; // TODO: Remove this after we have migrated to Heartwood.
  name: string;
  kind: Kind;
-
  lastCommit: CommitHeader;
}

export interface Blob {
-
  binary?: boolean;
-
  html?: boolean;
+
  binary: boolean;
  content: string;
  path: string;
-
  info: EntryInfo; // TODO: Remove this after we have migrated to Heartwood.
  name: string;
-
  kind: Kind;
  lastCommit: CommitHeader;
}

@@ -107,7 +76,7 @@ export interface Person {
}

export interface Peer {
-
  id: PeerId;
+
  id: string;
  person?: Person;
  delegate: boolean;
}
@@ -161,7 +130,6 @@ export class Project implements ProjectInfo {
  description: string;
  defaultBranch: string;
  delegates: string[];
-
  remotes: PeerId[]; // TODO: Remove this after we have migrated to Heartwood.
  seed: Seed;
  peers: Peer[];
  branches: Branches;
@@ -188,7 +156,6 @@ export class Project implements ProjectInfo {
    this.description = info.description;
    this.defaultBranch = info.defaultBranch;
    this.delegates = info.delegates;
-
    this.remotes = info.remotes; // TODO: Remove this after we have migrated to Heartwood.
    this.seed = seed;
    this.peers = peers;
    this.branches = branches;
@@ -211,14 +178,7 @@ export class Project implements ProjectInfo {
  }

  static async getInfo(nameOrId: string, host: Host): Promise<ProjectInfo> {
-
    if (window.HEARTWOOD) {
-
      return await new Request(`projects/${nameOrId}`, host).get();
-
    } else {
-
      const result = await new Request(`projects/${nameOrId}`, host).get();
-
      result["id"] = result["urn"];
-
      delete result["urn"];
-
      return result;
-
    }
+
    return await new Request(`projects/${nameOrId}`, host).get();
  }

  static async getProjects(
@@ -232,17 +192,7 @@ export class Project implements ProjectInfo {
      "per-page": opts?.perPage,
      page: opts?.page,
    };
-
    if (window.HEARTWOOD) {
-
      return await new Request("projects", host).get(params);
-
    } else {
-
      const results = await new Request("projects", host).get(params);
-
      results.forEach((result: { [x: string]: any }) => {
-
        result["id"] = result["urn"];
-
        delete result["urn"];
-
      });
-

-
      return results;
-
    }
+
    return await new Request("projects", host).get(params);
  }

  static async getDelegateProjects(
@@ -295,20 +245,6 @@ export class Project implements ProjectInfo {
    const result = await new Request(`projects/${id}/commits`, host).get(
      params,
    );
-
    if (!window.HEARTWOOD) {
-
      for (const commit of result.headers) {
-
        commit.commit = commit.header;
-
        delete commit.header;
-

-
        commit.commit.committer.time = commit.commit.committerTime;
-
        delete commit.commit.committerTime;
-

-
        commit.commit.id = commit.commit.sha1;
-
        delete commit.commit.sha1;
-
      }
-
      result.commits = result.headers;
-
      delete result.headers;
-
    }
    return result;
  }

@@ -325,45 +261,6 @@ export class Project implements ProjectInfo {
      this.seed.addr,
    ).get();

-
    if (!window.HEARTWOOD) {
-
      result.commit = result.header;
-
      delete result.header;
-

-
      result.commit.committer.time = result.commit.committerTime;
-
      delete result.commit.committerTime;
-

-
      result.commit.id = result.commit.sha1;
-
      delete result.commit.sha1;
-

-
      result.stats["insertions"] = result.stats["additions"];
-
      delete result.stats["additions"];
-
      result.diff["added"] = result.diff["created"];
-
      delete result.diff["created"];
-

-
      for (const kind of ["added", "deleted", "modified"]) {
-
        for (const file of result.diff[kind]) {
-
          for (const hunk of file.diff.hunks) {
-
            for (const line of hunk.lines) {
-
              if (line["lineNumOld"]) {
-
                line["lineNoOld"] = line["lineNumOld"];
-
                delete line["lineNumOld"];
-
              }
-

-
              if (line["lineNumNew"]) {
-
                line["lineNoNew"] = line["lineNumNew"];
-
                delete line["lineNumNew"];
-
              }
-

-
              if (line["lineNum"]) {
-
                line["lineNo"] = line["lineNum"];
-
                delete line["lineNum"];
-
              }
-
            }
-
          }
-
        }
-
      }
-
    }
-

    return result;
  }

@@ -373,17 +270,6 @@ export class Project implements ProjectInfo {
      `projects/${this.id}/tree/${commit}/${path}`,
      this.seed.addr,
    ).get();
-
    if (!window.HEARTWOOD) {
-
      if (result.info.lastCommit) {
-
        result.info.lastCommit.id = result.info.lastCommit.sha1;
-
        delete result.info.lastCommit.sha1;
-
      }
-
      for (const entry of result.entries) {
-
        if (entry.info.lastCommit) {
-
          entry.info.lastCommit.id = entry.info.lastCommit.sha1;
-
        }
-
      }
-
    }
    return result;
  }

@@ -392,10 +278,6 @@ export class Project implements ProjectInfo {
      `projects/${this.id}/blob/${commit}/${path}`,
      this.seed.addr,
    ).get();
-
    if (!window.HEARTWOOD) {
-
      result.info.lastCommit.id = result.info.lastCommit.sha1;
-
      delete result.info.lastCommit.sha1;
-
    }
    return result;
  }

@@ -404,10 +286,6 @@ export class Project implements ProjectInfo {
      `projects/${this.id}/readme/${commit}`,
      this.seed.addr,
    ).get();
-
    if (!window.HEARTWOOD && result.info.lastCommit) {
-
      result.info.lastCommit.id = result.info.lastCommit.sha1;
-
      delete result.info.lastCommit.sha1;
-
    }
    return result;
  }

@@ -416,31 +294,20 @@ export class Project implements ProjectInfo {
    seedHost: string,
    peer?: string,
  ): Promise<Project> {
-
    const [host, port] = seedHost.includes(":")
-
      ? seedHost.split(":")
-
      : [seedHost, defaultSeedPort];
+
    let seed: Seed | undefined = undefined;

-
    const seed = await Seed.lookup(host, Number(port)).catch(() => {
-
      throw new Error("Couldn't load project");
-
    });
-

-
    if (!seed?.valid) {
-
      throw new Error("Couldn't load project: invalid seed");
+
    try {
+
      seed = await Seed.lookup(utils.extractHost(seedHost));
+
    } catch (error) {
+
      throw new Error(`Couldn't load project: ${error}`);
    }

    const info = await Project.getInfo(id, seed.addr);
-
    id = isRadicleId(id) ? id : info.id;
+
    id = isRepositoryId(id) ? id : info.id;

    let peers: Peer[] = [];

-
    if (window.HEARTWOOD) {
-
      peers = await Project.getRemotes(id, seed.addr);
-
    } else {
-
      // Older versions of http-api don't include the ID.
-
      if (!info.id) info.id = id;
-

-
      peers = info.delegates ? await Project.getRemotes(id, seed.addr) : [];
-
    }
+
    peers = await Project.getRemotes(id, seed.addr);

    let remote: Remote = {
      heads: info.head ? { [info.defaultBranch]: info.head } : {},
@@ -458,15 +325,14 @@ export class Project implements ProjectInfo {
  }

  static async getMulti(
-
    projs: { nameOrId: Id; seed: string }[],
+
    projs: { nameOrId: string; seed: Host }[],
  ): Promise<ProjectResult[]> {
    const promises = [];

    for (const proj of projs) {
-
      const seed = { host: proj.seed, port: null };
      promises.push(
-
        Project.getInfo(proj.nameOrId, seed).then(info => {
-
          return { info, seed };
+
        Project.getInfo(proj.nameOrId, proj.seed).then(info => {
+
          return { info, seed: proj.seed };
        }),
      );
    }
modified src/lib/router.ts
@@ -252,10 +252,6 @@ export function routeToPath(route: Route) {
      return `${hostPrefix}/${route.params.id}${peer}/commits${suffix}`;
    } else if (route.params.view.resource === "history") {
      return `${hostPrefix}/${route.params.id}${peer}/history${suffix}`;
-
    } else if (route.params.view.resource === "patches") {
-
      return `${hostPrefix}/${route.params.id}${peer}/patches${suffix}`;
-
    } else if (route.params.view.resource === "patch") {
-
      return `${hostPrefix}/${route.params.id}${peer}/patches/${route.params.view.params.patch}`;
    } else if (
      route.params.view.resource === "issues" &&
      route.params.view.params?.view.resource === "new"
@@ -325,29 +321,6 @@ function resolveProjectRoute(
      search: undefined,
      route: segments.join("/"),
    };
-
  } else if (content === "patches") {
-
    const patch = segments.shift();
-
    if (patch) {
-
      return {
-
        view: { resource: "patch", params: { patch } },
-
        id,
-
        seed,
-
        peer,
-
        path: undefined,
-
        search: undefined,
-
        revision: undefined,
-
      };
-
    } else {
-
      return {
-
        view: { resource: "patches" },
-
        id,
-
        seed,
-
        peer,
-
        search: sanitizeQueryString(url.search),
-
        path: undefined,
-
        revision: undefined,
-
      };
-
    }
  } else if (content === "issues") {
    const issueOrAction = segments.shift();
    if (issueOrAction === "new") {
modified src/lib/router/definitions.ts
@@ -20,9 +20,7 @@ export interface ProjectsParams {
        params?: {
          view: { resource: "new" };
        };
-
      }
-
    | { resource: "patch"; params: { patch: string } }
-
    | { resource: "patches" };
+
      };
  seed: string;
  hash?: string;
  line?: string;
modified src/lib/search.ts
@@ -21,11 +21,15 @@ export async function searchProjectsAndProfiles(
  try {
    const projectOnSeeds = config.seeds.pinned.map(seed => ({
      nameOrId: query,
-
      seed: seed.host,
+
      seed: {
+
        host: seed.host,
+
        port: config.seeds.defaultHttpdPort,
+
        scheme: config.seeds.defaultHttpdScheme,
+
      },
    }));

    // The query is a radicle project ID.
-
    if (utils.isRadicleId(query)) {
+
    if (utils.isRepositoryId(query)) {
      const projects = await Project.getMulti(projectOnSeeds);

      if (projects.length === 0) {
modified src/lib/seed.ts
@@ -4,103 +4,32 @@ import * as proj from "@app/lib/project";
import { Request } from "@app/lib/api";
import { assert } from "@app/lib/error";
import { getSeedEmoji } from "@app/lib/utils";
-
import { isDomain } from "@app/lib/utils";
+
import { config } from "@app/lib/config";

export interface Stats {
  projects: { count: number };
  users: { count: number };
}

-
export class InvalidSeed {
-
  valid = false as const;
-

-
  host?: string;
-
  id?: string;
-

-
  constructor(host?: string, id?: string) {
-
    this.host = host;
-
    this.id = id;
-
  }
-
}
-

-
export const defaultSeedPort = window.HEARTWOOD ? 8080 : 8777;
-
export const defaultNodePort = 8776;
-
// TODO: Remove this after we have migrated to Heartwood.
-
export const defaultGitPort = 443;
-

export class Seed {
-
  valid = true as const;
-

-
  addr: { host: string; port: number | null };
-
  git: { host: string; port: number | null }; // TODO: Remove this after we have migrated to Heartwood.
-
  node: { host: string; id: string; port: number };
+
  addr: Host;
+
  node: Host & { id: string };

  version?: string;
  emoji: string;

-
  constructor(seed: {
-
    host: string;
-
    id: string;
-
    git?: string | null; // TODO: Remove this after we have migrated to Heartwood.
-
    addr?: string | null;
-
    version?: string | null;
-
  }) {
-
    assert(isDomain(seed.host), `invalid seed host: ${seed.host}`);
-
    if (window.HEARTWOOD) {
-
      assert(/^[a-zA-Z0-9]+$/.test(seed.id), `invalid seed id ${seed.id}`);
-
    } else {
-
      assert(/^[a-z0-9]+$/.test(seed.id), `invalid seed id ${seed.id}`);
-
    }
+
  constructor(seed: { host: Host; id: string; version?: string }) {
+
    assert(/^[a-zA-Z0-9]+$/.test(seed.id), `invalid seed id ${seed.id}`);

-
    let _seed = null;
-
    let _git = null; // TODO: Remove this after we have migrated to Heartwood.
-
    let _seedPort: number | null = defaultSeedPort;
-
    let _gitPort: number | null = defaultGitPort; // TODO: Remove this after we have migrated to Heartwood.
-

-
    if (seed.addr) {
-
      try {
-
        const url = new URL(seed.addr);
-
        _seed = url.hostname;
-

-
        if (url.port) {
-
          _seedPort = Number(url.port);
-
        } else if (url.protocol === "http:" && url.port === "") {
-
          _seedPort = 80;
-
        }
-
        if (url.protocol === "https:" && url.port === "") {
-
          _seedPort = 443;
-
        } else {
-
          _seedPort = null;
-
        }
-
      } catch {
-
        _seed = seed.addr;
-
      }
-
      assert(isDomain(_seed), `invalid seed host ${_seed}`);
-
    }
+
    this.emoji = getSeedEmoji(seed.host.host);

-
    if (window.HEARTWOOD) {
-
      if (seed.git) {
-
        try {
-
          const url = new URL(seed.git);
-
          _git = url.hostname;
-
          _gitPort = url.port ? Number(url.port) : null;
-
        } catch {
-
          _git = seed.git;
-
        }
-
        assert(isDomain(_git), `invalid seed git host ${_git}`);
-
      }
-
    }
-

-
    this.emoji = getSeedEmoji(seed.host);
-

-
    // The `_seed` being more specific takes
-
    // precedence over the `host`, if available.
-
    _seed = _seed ?? seed.host;
-
    _git = _git ?? seed.host; // TODO: Remove this after we have migrated to Heartwood.
-

-
    this.addr = { host: _seed, port: _seedPort };
-
    this.git = { host: _git, port: _gitPort }; // TODO: Remove this after we have migrated to Heartwood.
-
    this.node = { host: seed.host, id: seed.id, port: defaultNodePort };
+
    this.addr = seed.host;
+
    this.node = {
+
      host: seed.host.host,
+
      id: seed.id,
+
      port: config.seeds.defaultNodePort,
+
      scheme: seed.host.scheme,
+
    };

    if (seed.version) {
      this.version = seed.version;
@@ -128,14 +57,7 @@ export class Seed {
      ? await proj.Project.getDelegateProjects(id, this.addr, { perPage })
      : await proj.Project.getProjects(this.addr, { perPage });

-
    if (window.HEARTWOOD) {
-
      return result;
-
    } else {
-
      return result.map((project: proj.ProjectInfo) => ({
-
        ...project,
-
        id: project.id,
-
      }));
-
    }
+
    return result;
  }

  async getStats(): Promise<{
@@ -146,36 +68,23 @@ export class Seed {
  }

  static async getNode(host: Host): Promise<{ id: string }> {
-
    if (window.HEARTWOOD) {
-
      return new Request("/node", host).get();
-
    } else {
-
      return new Request("/peer", host).get();
-
    }
+
    return new Request("/node", host).get();
  }

  static async getInfo(host: Host): Promise<{ version: string }> {
    return new Request("/", host).get();
  }

-
  static async lookup(
-
    hostname: string,
-
    port: number = defaultSeedPort,
-
  ): Promise<Seed> {
-
    const host = { host: hostname, port };
+
  static async lookup(host: Host): Promise<Seed> {
    const [info, node] = await Promise.all([
      Seed.getInfo(host),
      Seed.getNode(host),
    ]);

    return new Seed({
-
      host: hostname,
      id: node.id,
      version: info.version,
-
      addr: `https://${host.host}:${host.port}`,
+
      host,
    });
  }
-

-
  static async lookupMulti(hostnames: string[]): Promise<Seed[]> {
-
    return await Promise.all(hostnames.map(h => Seed.lookup(h)));
-
  }
}
modified src/lib/utils.ts
@@ -1,8 +1,9 @@
+
import type { Host } from "@app/lib/api";
+

import md5 from "md5";
import bs58 from "bs58";
import twemojiModule from "twemoji";

-
import { assert } from "@app/lib/error";
import { base } from "@app/lib/router";
import { config } from "@app/lib/config";

@@ -43,23 +44,7 @@ export function formatLocationHash(hash: string | null): number | null {
  return null;
}

-
export function formatSeedId(id: string): string {
-
  return id.substring(0, 6) + "…" + id.substring(id.length - 6, id.length);
-
}
-

-
export function formatRadicleId(id: string): string {
-
  assert(isRadicleId(id));
-

-
  if (window.HEARTWOOD) {
-
    return id.substring(0, 10) + "…" + id.substring(id.length - 6, id.length);
-
  } else {
-
    return id.substring(0, 14) + "…" + id.substring(id.length - 6, id.length);
-
  }
-
}
-

-
// Parses a NID into an object of prefix and pubkey,
-
// since prefix can be undefined
-
export function parseNid(
+
export function parseNodeId(
  nid: string,
): { prefix: string; pubkey: string } | undefined {
  const match = /^(did:key:)?(z[a-zA-Z0-9]+)$/.exec(nid);
@@ -77,19 +62,52 @@ export function parseNid(
  return undefined;
}

-
// Format a Node Identifier (NID), also represents users or peers
-
export function formatNodeId(nid: string): string {
-
  const parsedNid = parseNid(nid);
-
  if (parsedNid) {
-
    const { prefix, pubkey } = parsedNid;
-
    const formattedPubKey =
-
      pubkey.substring(0, 6) +
-
      "…" +
-
      pubkey.substring(pubkey.length - 6, pubkey.length);
-
    return `${prefix}${formattedPubKey}`;
+
export function parseRepositoryId(
+
  rid: string,
+
): { prefix: string; pubkey: string } | undefined {
+
  const match = /^(rad:)?(z[a-zA-Z0-9]+)$/.exec(rid);
+
  if (match) {
+
    const hex = bs58.decode(match[2].substring(1));
+
    if (hex.byteLength !== 20) {
+
      return undefined;
+
    }
+

+
    return { prefix: match[1] || "rad:", pubkey: match[2] };
+
  }
+

+
  return undefined;
+
}
+

+
export function isNodeId(input: string): boolean {
+
  return Boolean(parseNodeId(input));
+
}
+

+
export function isRepositoryId(input: string): boolean {
+
  return Boolean(parseRepositoryId(input));
+
}
+

+
export function formatNodeId(id: string): string {
+
  const parsedId = parseNodeId(id);
+

+
  if (parsedId) {
+
    return truncateId(parsedId.prefix, parsedId.pubkey);
  }

-
  return nid;
+
  return id;
+
}
+

+
export function formatRepositoryId(id: string): string {
+
  const parsedId = parseRepositoryId(id);
+

+
  if (parsedId) {
+
    return truncateId(parsedId.prefix, parsedId.pubkey);
+
  }
+

+
  return id;
+
}
+

+
function truncateId(prefix: string, pubkey: string): string {
+
  return `${prefix}${pubkey.substring(0, 6) + "…" + pubkey.slice(-6)}`;
}

export function formatCommit(oid: string): string {
@@ -191,20 +209,6 @@ export const formatTimestamp = (
  return new Date(timestamp).toUTCString();
};

-
// Check whether the input is a Radicle ID.
-
export function isRadicleId(input: string): boolean {
-
  if (window.HEARTWOOD) {
-
    return /^(did:key:)?[a-zA-Z0-9]+$/.test(input);
-
  } else {
-
    return /^rad:[a-z]+:[a-zA-Z0-9]+$/.test(input);
-
  }
-
}
-

-
// Check whether the input is a Radicle Peer ID.
-
export function isPeerId(input: string): boolean {
-
  return /^h[a-zA-Z0-9]+$/.test(input);
-
}
-

// Check whether the input is a SHA1 commit.
export function isOid(input: string): boolean {
  return /^[a-fA-F0-9]{40}$/.test(input);
@@ -221,15 +225,6 @@ export function isFulfilled<T>(
  return input.status === "fulfilled";
}

-
// Parse a Radicle Id.
-
export function parseRadicleId(id: string): string {
-
  if (window.HEARTWOOD) {
-
    return id.replace(/^rad:/, "");
-
  } else {
-
    return id.replace(/^rad:[a-z]+:/, "");
-
  }
-
}
-

// Get amount of days passed between two dates without including the end date
export function getDaysPassed(from: Date, to: Date): number {
  return Math.floor((to.getTime() - from.getTime()) / (24 * 60 * 60 * 1000));
@@ -257,8 +252,8 @@ export function isMarkdownPath(path: string): boolean {
  return /\.(md|mkd|markdown)$/i.test(path);
}

-
// Check whether the given input string is a domain, eg. `alt-clients.radicle.xyz.
-
// Also accepts in dev env 0.0.0.0 as domain
+
// Check whether the given input string is a domain, eg. seed.radicle.xyz.
+
// Also accepts in dev env 0.0.0.0 as domain.
export function isDomain(input: string): boolean {
  return (
    (/^[a-z][a-z0-9.-]+$/.test(input) && /\.[a-z]+$/.test(input)) ||
@@ -268,7 +263,11 @@ export function isDomain(input: string): boolean {

// Check whether the given address is a local host address.
export function isLocal(addr: string): boolean {
-
  return addr === "127.0.0.1" || addr === "0.0.0.0";
+
  return (
+
    addr.startsWith("127.0.0.1") ||
+
    addr.startsWith("0.0.0.0") ||
+
    addr.startsWith("radicle.local")
+
  );
}

// Get the gravatar URL of an email.
@@ -299,3 +298,28 @@ export function twemoji(
    className: `txt-emoji`,
  });
}
+

+
export function extractHost(origin: string): Host {
+
  if (
+
    origin === "radicle.local" ||
+
    origin === "radicle.local:8080" ||
+
    origin === "0.0.0.0" ||
+
    origin === "0.0.0.0:8080" ||
+
    origin === "127.0.0.1" ||
+
    origin === "127.0.0.1:8080"
+
  ) {
+
    return { host: "0.0.0.0", port: 8080, scheme: "http" };
+
  } else if (origin.includes(":")) {
+
    return {
+
      host: origin.split(":")[0],
+
      port: Number(origin.split(":")[1]),
+
      scheme: config.seeds.defaultHttpdScheme,
+
    };
+
  } else {
+
    return {
+
      host: origin,
+
      port: config.seeds.defaultHttpdPort,
+
      scheme: config.seeds.defaultHttpdScheme,
+
    };
+
  }
+
}
modified src/views/home/Index.svelte
@@ -21,7 +21,11 @@
      ? Project.getMulti(
          config.projects.pinned.map(project => ({
            nameOrId: project.id,
-
            seed: project.seed,
+
            seed: {
+
              host: project.seed,
+
              port: config.seeds.defaultHttpdPort,
+
              scheme: config.seeds.defaultHttpdScheme,
+
            },
          })),
        )
      : Promise.resolve([]);
modified src/views/projects/Blob.svelte
@@ -18,7 +18,7 @@
  export let line: string | undefined = undefined;

  const fileExtension = blob.path.split(".").pop() ?? "";
-
  const lastCommit = window.HEARTWOOD ? blob.lastCommit : blob.info.lastCommit;
+
  const lastCommit = blob.lastCommit;

  const parentDir = blob.path
    .match(/^.*\/|/)
@@ -215,7 +215,7 @@
      <span class="file-name">
        <span class="txt-faded">{parentDir}</span>
        &#8203;
-
        <span>{window.HEARTWOOD ? blob.name : blob.info.name}</span>
+
        <span>{blob.name}</span>
      </span>
      <div class="right">
        {#if isMarkdown}
modified src/views/projects/CloneButton.svelte
@@ -1,14 +1,17 @@
<script lang="ts">
-
  import * as utils from "@app/lib/utils";
  import Clipboard from "@app/components/Clipboard.svelte";
  import Floating from "@app/components/Floating.svelte";
  import { closeFocused } from "@app/components/Floating.svelte";
+
  import { parseRepositoryId } from "@app/lib/utils";

  export let seedHost: string;
  export let id: string;
+
  export let name: string;

-
  $: radCloneUrl = `rad clone rad://${seedHost}/${utils.parseRadicleId(id)}`;
-
  $: gitCloneUrl = `https://${seedHost}/${utils.parseRadicleId(id)}.git`;
+
  $: radCloneUrl = `rad clone ${id}`;
+
  $: gitCloneUrl = `git clone https://${seedHost}/${
+
    parseRepositoryId(id)?.pubkey ?? id
+
  }.git ${name}`;
</script>

<style>
modified src/views/projects/Commit.svelte
@@ -74,7 +74,7 @@
    </div>
  </header>
  <Changeset
-
    stats={window.HEARTWOOD ? commit.diff.stats : commit.stats}
+
    stats={commit.diff.stats}
    diff={commit.diff}
    on:browse={onBrowse} />
</div>
modified src/views/projects/Commit/CommitTeaser.svelte
@@ -5,7 +5,6 @@

  import Icon from "@app/components/Icon.svelte";
  import CommitAuthorship from "./CommitAuthorship.svelte";
-
  import CommitVerifiedBadge from "./CommitVerifiedBadge.svelte";

  export let commit: CommitMetadata;

@@ -99,11 +98,6 @@
    <CommitAuthorship {commit} noDelegate />
  </div>
  <div class="column-right">
-
    {#if !window.HEARTWOOD && commit.context.committer}
-
      <div class="layout-desktop">
-
        <CommitVerifiedBadge {commit} />
-
      </div>
-
    {/if}
    <span class="hash txt-highlight">{formatCommit(commit.commit.id)}</span>
    <!-- svelte-ignore a11y-click-events-have-key-events -->
    <div
modified src/views/projects/Header.svelte
@@ -6,9 +6,10 @@
  import * as router from "@app/lib/router";
  import BranchSelector from "@app/views/projects/BranchSelector.svelte";
  import CloneButton from "@app/views/projects/CloneButton.svelte";
+
  import HeaderToggleLabel from "@app/views/projects/HeaderToggleLabel.svelte";
  import PeerSelector from "@app/views/projects/PeerSelector.svelte";
  import { closeFocused } from "@app/components/Floating.svelte";
-
  import HeaderToggleLabel from "@app/views/projects/HeaderToggleLabel.svelte";
+
  import { config } from "@app/lib/config";

  export let activeRoute: ProjectRoute;
  export let project: Project;
@@ -21,7 +22,7 @@

  // Switches between project views.
  const toggleContent = (
-
    input: "patches" | "issues" | "history",
+
    input: "issues" | "history",
    keepSourceInPath: boolean,
  ) => {
    router.updateProjectRoute({
@@ -51,7 +52,7 @@
  };

  function goToSeed() {
-
    if (seed.addr.port) {
+
    if (seed.addr.port !== config.seeds.defaultHttpdPort) {
      router.push({
        resource: "seeds",
        params: { host: `${seed.addr.host}:${seed.addr.port}` },
@@ -98,10 +99,8 @@
    {revision}
    on:branchChanged={event => updateRevision(event.detail)} />

-
  {#if window.HEARTWOOD && seed.addr.host}
-
    <CloneButton seedHost={seed.addr.host} {id} />
-
  {:else if seed.git.host}
-
    <CloneButton seedHost={seed.git.host} {id} />
+
  {#if seed.addr.host}
+
    <CloneButton seedHost={seed.addr.host} {id} name={project.name} />
  {/if}

  <span>
@@ -131,14 +130,6 @@
    <span class="txt-bold">{project.issues.open ?? 0}</span>
    issue(s)
  </HeaderToggleLabel>
-
  <HeaderToggleLabel
-
    ariaLabel="Patch count"
-
    clickable
-
    active={activeRoute.params.view.resource === "patches"}
-
    on:click={() => toggleContent("patches", false)}>
-
    <span class="txt-bold">{project.patches.proposed ?? 0}</span>
-
    patch(es)
-
  </HeaderToggleLabel>
  <HeaderToggleLabel ariaLabel="Contributor count">
    <span class="txt-bold">{tree.stats.contributors}</span>
    contributor(s)
modified src/views/projects/Issue.svelte
@@ -93,9 +93,6 @@
    color: var(--color-negative);
    background-color: var(--color-negative-2);
  }
-
  .replies {
-
    margin-left: 2rem;
-
  }
  .assignee {
    display: flex;
    flex-direction: row;
@@ -142,21 +139,8 @@
  </header>
  <main>
    <div class="comments">
-
      {#if !window.HEARTWOOD}
-
        <Comment comment={issue.comment} {getImage} />
-
      {/if}
-

      {#each issue.discussion as comment}
        <Comment {comment} {getImage} />
-
        {#if !window.HEARTWOOD}
-
          {#if comment.replies}
-
            <div class="replies">
-
              {#each comment.replies as reply}
-
                <Comment comment={reply} {getImage} />
-
              {/each}
-
            </div>
-
          {/if}
-
        {/if}
      {/each}
    </div>
    <div class="metadata layout-desktop">
modified src/views/projects/Issue/New.svelte
@@ -11,7 +11,7 @@
  import TextInput from "@app/components/TextInput.svelte";
  import Textarea from "@app/components/Textarea.svelte";
  import { Issue } from "@app/lib/issue";
-
  import { canonicalize, formatNodeId, parseNid } from "@app/lib/utils";
+
  import { canonicalize, formatNodeId, parseNodeId } from "@app/lib/utils";
  import { createEventDispatcher } from "svelte";

  export let session: Session;
@@ -29,13 +29,13 @@
  let preview: boolean = false;

  function handleAddAssignee() {
-
    const nid = parseNid(assignee);
-
    if (nid) {
-
      if (assignees.includes(nid.pubkey)) {
+
    const nodeId = parseNodeId(assignee);
+
    if (nodeId) {
+
      if (assignees.includes(nodeId.pubkey)) {
        assigneeCaption = "This user is already assigned";
        return;
      }
-
      assignees.push(nid.pubkey);
+
      assignees.push(nodeId.pubkey);
      assignees = assignees;
      assignee = "";
    } else {
@@ -185,7 +185,6 @@
              author: { id: session.publicKey },
              body: issueText,
              reactions: {},
-
              replies: null,
              replyTo: null,
              timestamp: Date.now(),
            }}
@@ -245,7 +244,7 @@
            on:input={() => (assigneeCaption = undefined)}
            placeholder="Assign this issue"
            validationMessage={assigneeCaption}
-
            valid={Boolean(parseNid(assignee))} />
+
            valid={Boolean(parseNodeId(assignee))} />
        </div>
      </div>
      <div class="section txt-small">
modified src/views/projects/Issues.svelte
@@ -8,6 +8,7 @@
  import type { Tab } from "@app/components/TabBar.svelte";

  import * as router from "@app/lib/router";
+
  import * as utils from "@app/lib/utils";
  import { capitalize } from "@app/lib/utils";
  import { groupIssues } from "@app/lib/issue";
  import { sessionStore } from "@app/lib/session";
@@ -79,7 +80,7 @@
        active={state} />
    </div>
    <HeaderToggleLabel
-
      disabled={!$sessionStore}
+
      disabled={!$sessionStore || !utils.isLocal(project.seed.host)}
      on:click={() => {
        router.updateProjectRoute({
          view: {
deleted src/views/projects/Patch.svelte
@@ -1,151 +0,0 @@
-
<script lang="ts">
-
  import type { Project } from "@app/lib/project";
-

-
  import { capitalize } from "@app/lib/utils";
-
  import { Patch, PatchTab } from "@app/lib/patch";
-
  import { formatObjectId } from "@app/lib/cobs";
-
  import Authorship from "@app/components/Authorship.svelte";
-

-
  import Changeset from "./SourceBrowser/Changeset.svelte";
-
  import PatchSideBar from "./Patch/PatchSideBar.svelte";
-
  import PatchTabBar from "./Patch/PatchTabBar.svelte";
-
  import PatchTimeline from "./Patch/PatchTimeline.svelte";
-
  import Placeholder from "@app/components/Placeholder.svelte";
-
  import * as router from "@app/lib/router";
-

-
  export let patch: Patch;
-
  export let project: Project;
-

-
  const onSwitch = ({ detail }: { detail: PatchTab }) => {
-
    activeTab = detail;
-
  };
-

-
  const onRevisionChanged = ({ detail }: { detail: string }) => {
-
    revisionNumber = parseInt(detail);
-
  };
-

-
  const onBrowse = (event: { detail: string }, revision: string) => {
-
    router.updateProjectRoute({
-
      view: { resource: "tree" },
-
      revision,
-
      path: event.detail,
-
    });
-
  };
-

-
  let activeTab = PatchTab.Timeline;
-
  let revisionNumber = patch.revisions.length - 1;
-

-
  $: revision = patch.revisions[revisionNumber];
-
</script>
-

-
<style>
-
  header {
-
    padding: 1rem;
-
    background: var(--color-foreground-1);
-
    border-radius: var(--border-radius);
-
  }
-
  .patch {
-
    padding: 0 2rem 0 8rem;
-
  }
-
  .summary {
-
    display: flex;
-
    justify-content: space-between;
-
    flex-direction: row;
-
    align-items: center;
-
    margin-bottom: 0.5rem;
-
  }
-
  .summary-left {
-
    display: flex;
-
    flex-direction: column;
-
  }
-
  .summary-title {
-
    display: flex;
-
    margin-right: 0.75rem;
-
  }
-
  .id {
-
    font-size: var(--font-size-tiny);
-
    color: var(--color-foreground-5);
-
  }
-
  .summary-state {
-
    padding: 0.5rem 1rem;
-
    border-radius: 1.25rem;
-
  }
-
  .proposed {
-
    color: var(--color-positive-6);
-
    background-color: var(--color-positive-1);
-
  }
-
  .draft {
-
    color: var(--color-positive-6);
-
    background-color: var(--color-positive-1);
-
  }
-
  .archived {
-
    color: var(--color-negative-6);
-
    background-color: var(--color-negative-1);
-
  }
-
  .flex {
-
    display: flex;
-
  }
-

-
  @media (max-width: 960px) {
-
    .patch {
-
      padding-left: 2rem;
-
    }
-
  }
-
</style>
-

-
<div class="patch">
-
  <header>
-
    <div class="summary">
-
      <div class="summary-left">
-
        <span class="summary-title txt-medium">
-
          {patch.title}
-
        </span>
-
        <span class="txt-monospace id layout-desktop">{patch.id}</span>
-
        <span class="txt-monospace id layout-mobile">
-
          {formatObjectId(patch.id)}
-
        </span>
-
      </div>
-
      <div
-
        class="summary-state"
-
        class:proposed={patch.state === "proposed"}
-
        class:draft={patch.state === "draft"}
-
        class:archived={patch.state === "archived"}>
-
        {capitalize(patch.state)}
-
      </div>
-
    </div>
-
    <Authorship
-
      author={patch.author}
-
      timestamp={patch.timestamp}
-
      caption="opened" />
-
  </header>
-

-
  <PatchTabBar
-
    {activeTab}
-
    {revisionNumber}
-
    revisions={patch.revisions}
-
    on:switchTab={onSwitch}
-
    on:revisionChanged={onRevisionChanged} />
-

-
  <main>
-
    {#if activeTab === PatchTab.Timeline}
-
      <div class="flex">
-
        <PatchTimeline {patch} {revisionNumber} {project} />
-
        <PatchSideBar {patch} />
-
      </div>
-
    {:else if activeTab === PatchTab.Diff && revision.changeset}
-
      <Changeset
-
        stats={window.HEARTWOOD
-
          ? revision.changeset.diff.stats
-
          : revision.changeset.stats}
-
        diff={revision.changeset.diff}
-
        on:browse={e => onBrowse(e, revision.oid)} />
-
    {:else if activeTab === PatchTab.Diff}
-
      <Placeholder emoji="🍳">
-
        <span slot="title">No changeset found</span>
-
        <span slot="body">
-
          We couldn't find a changeset related to this patch or revision
-
        </span>
-
      </Placeholder>
-
    {/if}
-
  </main>
-
</div>
deleted src/views/projects/Patch/PatchSideBar.svelte
@@ -1,54 +0,0 @@
-
<script lang="ts">
-
  import type { Patch } from "@app/lib/patch";
-

-
  export let patch: Patch;
-
</script>
-

-
<style>
-
  .metadata {
-
    flex-basis: 18rem;
-
    margin-left: 1rem;
-
    border-radius: var(--border-radius-medium);
-
    font-size: var(--font-size-small);
-
    padding-left: 1rem;
-
  }
-
  .metadata-section {
-
    margin-bottom: 1rem;
-
    border-bottom: 1px dashed var(--color-foreground-4);
-
  }
-
  .metadata-section-header {
-
    font-size: var(--font-size-small);
-
    margin-bottom: 0.75rem;
-
    color: var(--color-foreground-5);
-
  }
-
  .metadata-section-body {
-
    margin-bottom: 1.25rem;
-
  }
-
  .metadata-section-empty {
-
    color: var(--color-foreground-6);
-
  }
-
  .label {
-
    border-radius: var(--border-radius);
-
    color: var(--color-tertiary);
-
    background-color: var(--color-tertiary-2);
-
    padding: 0.25rem 0.75rem;
-
    margin-right: 0.5rem;
-
    font-size: var(--font-size-small);
-
    line-height: 1.6;
-
  }
-
</style>
-

-
<div class="metadata layout-desktop">
-
  <div class="metadata-section">
-
    <div class="metadata-section-header">Labels</div>
-
    <div class="metadata-section-body">
-
      {#if patch.labels?.length}
-
        {#each patch.labels as label}
-
          <span class="label">{label}</span>
-
        {/each}
-
      {:else}
-
        <div class="metadata-section-empty">No labels.</div>
-
      {/if}
-
    </div>
-
  </div>
-
</div>
deleted src/views/projects/Patch/PatchTabBar.svelte
@@ -1,102 +0,0 @@
-
<script lang="ts" strictEvents>
-
  import type { Tab } from "@app/components/TabBar.svelte";
-

-
  import Dropdown from "@app/components/Dropdown.svelte";
-
  import Floating, { closeFocused } from "@app/components/Floating.svelte";
-
  import TabBar from "@app/components/TabBar.svelte";
-

-
  import type { Revision } from "@app/lib/patch";
-
  import { PatchTab } from "@app/lib/patch";
-
  import { formatCommit, formatTimestamp } from "@app/lib/utils";
-
  import { createEventDispatcher } from "svelte";
-

-
  export let revisions: Revision[];
-
  export let revisionNumber: number;
-
  export let activeTab: PatchTab;
-

-
  const dispatch = createEventDispatcher<{
-
    switchTab: PatchTab;
-
    revisionChanged: string;
-
  }>();
-

-
  const formatRevisionName = (revision: Revision, index: number) => {
-
    return `R${index} ${formatCommit(revision.oid)} ${formatTimestamp(
-
      revision.timestamp,
-
    )}`;
-
  };
-

-
  const revisionList = Object.values(revisions).map((b, i) => ({
-
    key: formatRevisionName(b, i),
-
    value: i.toString(),
-
    title: `Browse revision ${formatRevisionName(b, i)}`,
-
    badge: null,
-
  }));
-

-
  const onRevisionChange = ({ detail }: { detail: string }) => {
-
    closeFocused();
-
    dispatch("revisionChanged", detail);
-
  };
-

-
  let options: Tab<PatchTab>[];
-
  $: options = [
-
    {
-
      title: "Patch",
-
      value: PatchTab.Timeline,
-
    },
-
    {
-
      title: "Changeset",
-
      value: PatchTab.Diff,
-
    },
-
  ];
-
</script>
-

-
<style>
-
  .bar {
-
    align-items: center;
-
    display: flex;
-
    gap: 1rem;
-
    margin: 1.5rem 0;
-
  }
-
  .revision-toggle {
-
    border-radius: var(--border-radius-small);
-
    border: none;
-
    color: var(--color-foreground-6);
-
    font-family: var(--font-family-monospace);
-
    font-size: var(--font-size-tiny);
-
    height: var(--button-tiny-height);
-
    padding: 0.25rem 0.5rem;
-
    background-color: var(--color-background);
-
  }
-
  .revision-toggle:hover {
-
    background-color: var(--color-foreground-1);
-
    color: var(--color-foreground);
-
  }
-
  .revision-toggle:disabled {
-
    color: var(--color-foreground-5);
-
  }
-
</style>
-

-
<div class="bar txt-small">
-
  <TabBar
-
    {options}
-
    on:select={e => {
-
      dispatch("switchTab", e.detail);
-
    }}
-
    active={activeTab} />
-

-
  <Floating disabled={revisions.length <= 1}>
-
    <button
-
      slot="toggle"
-
      class="txt-small revision-toggle"
-
      disabled={revisions.length <= 1}>
-
      {formatRevisionName(revisions[revisionNumber], revisionNumber)}
-
    </button>
-

-
    <svelte:fragment slot="modal">
-
      <Dropdown
-
        items={revisionList}
-
        selected={revisionNumber.toString()}
-
        on:select={onRevisionChange} />
-
    </svelte:fragment>
-
  </Floating>
-
</div>
deleted src/views/projects/Patch/PatchTeaser.svelte
@@ -1,114 +0,0 @@
-
<script lang="ts">
-
  import type { Patch } from "@app/lib/patch";
-

-
  import { formatObjectId } from "@app/lib/cobs";
-
  import { twemoji } from "@app/lib/utils";
-

-
  import Authorship from "@app/components/Authorship.svelte";
-

-
  export let patch: Patch;
-

-
  const commentCount = patch.countComments(patch.revisions.length - 1);
-
</script>
-

-
<style>
-
  .patch-teaser {
-
    display: flex;
-
    align-items: center;
-
    justify-content: space-between;
-
    background-color: var(--color-foreground-1);
-
    padding: 0.75rem 0;
-
  }
-
  .patch-teaser:hover {
-
    background-color: var(--color-foreground-2);
-
    cursor: pointer;
-
  }
-
  .patch-id {
-
    color: var(--color-foreground-5);
-
    font-size: var(--font-size-tiny);
-
    font-family: var(--font-family-monospace);
-
    margin-left: 0.5rem;
-
  }
-

-
  .column-left {
-
    flex: min-content;
-
  }
-
  .column-right {
-
    display: flex;
-
    align-items: center;
-
    justify-content: flex-end;
-
    margin-right: 1rem;
-
    flex-basis: 5rem;
-
  }
-
  .comment-count {
-
    color: var(--color-foreground-4);
-
    font-weight: var(--font-weight-bold);
-
  }
-
  .comment-count .emoji {
-
    margin-right: 0.25rem;
-
  }
-

-
  .state {
-
    padding: 0 1rem;
-
  }
-
  .state-icon {
-
    width: 0.5rem;
-
    height: 0.5rem;
-
    border-radius: 0.5rem;
-
  }
-
  .open {
-
    background-color: var(--color-positive);
-
  }
-
  .closed {
-
    background-color: var(--color-negative);
-
  }
-
  .summary {
-
    display: flex;
-
    flex-direction: row;
-
    align-items: center;
-
    overflow: hidden;
-
    white-space: nowrap;
-
    text-overflow: ellipsis;
-
    padding-right: 1rem;
-
  }
-

-
  @media (max-width: 720px) {
-
    .column-left {
-
      overflow: hidden;
-
    }
-
    .summary {
-
      overflow: hidden;
-
      white-space: nowrap;
-
      text-overflow: ellipsis;
-
      padding-right: 1rem;
-
    }
-
  }
-
</style>
-

-
<div class="patch-teaser">
-
  <div class="state">
-
    <div
-
      class="state-icon"
-
      class:closed={patch.state === "archived"}
-
      class:open={patch.state === "proposed"} />
-
  </div>
-
  <div class="column-left">
-
    <div class="summary">
-
      <!-- TODO: Truncation not working on overflow -->
-
      {patch.title}
-
      <span class="patch-id">{formatObjectId(patch.id)}</span>
-
    </div>
-
    <Authorship
-
      caption="opened"
-
      author={patch.author}
-
      timestamp={patch.timestamp} />
-
  </div>
-
  {#if commentCount > 0}
-
    <div class="column-right">
-
      <div class="comment-count">
-
        <span class="txt-tiny emoji" use:twemoji>💬</span>
-
        <span>{commentCount}</span>
-
      </div>
-
    </div>
-
  {/if}
-
</div>
deleted src/views/projects/Patch/PatchTimeline.svelte
@@ -1,73 +0,0 @@
-
<script lang="ts">
-
  import type { Blob, Project } from "@app/lib/project";
-

-
  import Authorship from "@app/components/Authorship.svelte";
-
  import Comment from "@app/components/Comment.svelte";
-
  import Review from "@app/views/projects/Patch/Review.svelte";
-
  import { Patch, TimelineType } from "@app/lib/patch";
-
  import { canonicalize } from "@app/lib/utils";
-
  import { formatSeedId } from "@app/lib/utils";
-

-
  export let patch: Patch;
-
  export let revisionNumber: number;
-
  export let project: Project;
-

-
  $: timeline = patch.createTimeline(revisionNumber);
-

-
  // Get an image blob based on a relative path.
-
  const getImage = async (imagePath: string): Promise<Blob> => {
-
    const finalPath = canonicalize(imagePath, "/"); // We only use the root path in issues.
-
    const commit = project.branches[project.defaultBranch]; // We suppose that all issues are only looked at on HEAD of the default branch.
-
    return project.getBlob(commit, finalPath);
-
  };
-
</script>
-

-
<style>
-
  .timeline {
-
    display: flex;
-
    flex-direction: column;
-
    flex: 1;
-
  }
-
  .replies {
-
    margin-left: 2rem;
-
  }
-
  .element {
-
    margin: 0 0 1rem 3rem;
-
  }
-
</style>
-

-
<div class="timeline">
-
  {#each timeline as element}
-
    {#if element.type === TimelineType.Merge && element.inner.peer.person}
-
      <div class="element">
-
        <Authorship
-
          author={{ id: element.inner.peer.person.id }}
-
          caption={`merged to ${formatSeedId(element.inner.peer.id)}`}
-
          timestamp={element.timestamp} />
-
      </div>
-
    {:else if element.type === TimelineType.Review && element.inner.author.id}
-
      <div class="margin-left">
-
        <Review review={element.inner} {getImage} />
-
      </div>
-
    {:else if element.type === TimelineType.Comment}
-
      <div class="margin-left">
-
        <!-- Since the element variable only experiences changes on the inner property,
-
        this component has to be forced to be rerendered when element.inner changes -->
-
        {#key element.inner}
-
          <Comment comment={element.inner} {getImage} />
-
        {/key}
-
      </div>
-
    {:else if element.type === TimelineType.Thread}
-
      <div class="margin-left">
-
        <Comment comment={element.inner} {getImage} />
-
        {#if !window.HEARTWOOD}
-
          <div class="replies">
-
            {#each element.inner.replies as comment}
-
              <Comment caption="replied" {comment} {getImage} />
-
            {/each}
-
          </div>
-
        {/if}
-
      </div>
-
    {/if}
-
  {/each}
-
</div>
deleted src/views/projects/Patch/Review.svelte
@@ -1,31 +0,0 @@
-
<script lang="ts">
-
  import type { Review } from "@app/lib/patch";
-
  import type { Blob } from "@app/lib/project";
-

-
  import Authorship from "@app/components/Authorship.svelte";
-
  import Comment from "@app/components/Comment.svelte";
-
  import { formatVerdict } from "@app/lib/patch";
-

-
  export let review: Review;
-
  export let getImage: (path: string) => Promise<Blob>;
-
</script>
-

-
<style>
-
  div {
-
    margin: 0 0 1rem 3rem;
-
  }
-
</style>
-

-
{#if review.comment.body}
-
  <Comment
-
    {getImage}
-
    comment={review.comment}
-
    caption={formatVerdict(review.verdict)} />
-
{:else}
-
  <div>
-
    <Authorship
-
      author={review.author}
-
      timestamp={review.timestamp}
-
      caption={formatVerdict(review.verdict)} />
-
  </div>
-
{/if}
deleted src/views/projects/Patches.svelte
@@ -1,94 +0,0 @@
-
<script lang="ts" context="module">
-
  export type State = "proposed" | "draft" | "archived";
-
</script>
-

-
<script lang="ts">
-
  import type { Patch } from "@app/lib/patch";
-
  import type { Tab } from "@app/components/TabBar.svelte";
-
  import type { Project } from "@app/lib/project";
-

-
  import PatchTeaser from "./Patch/PatchTeaser.svelte";
-
  import Placeholder from "@app/components/Placeholder.svelte";
-
  import TabBar from "@app/components/TabBar.svelte";
-

-
  import { capitalize } from "@app/lib/utils";
-
  import { groupPatches } from "@app/lib/patch";
-
  import * as router from "@app/lib/router";
-

-
  export let state: State;
-
  export let patches: Patch[];
-
  export let project: Project;
-

-
  let options: Tab<State>[];
-
  const sortedPatches = groupPatches(patches);
-

-
  $: filteredPatches = sortedPatches[state];
-
  $: options = [
-
    {
-
      value: "proposed",
-
      count: project.patches.proposed,
-
    },
-
    {
-
      value: "draft",
-
      count: project.patches.draft,
-
    },
-
    {
-
      value: "archived",
-
      count: project.patches.archived,
-
    },
-
  ];
-
</script>
-

-
<style>
-
  .patches {
-
    padding: 0 2rem 0 8rem;
-
    font-size: var(--font-size-small);
-
  }
-
  .patches-list {
-
    border-radius: var(--border-radius-small);
-
    overflow: hidden;
-
  }
-
  .teaser:not(:last-child) {
-
    border-bottom: 1px dashed var(--color-background);
-
  }
-

-
  @media (max-width: 960px) {
-
    .patches {
-
      padding-left: 2rem;
-
    }
-
  }
-
</style>
-

-
<div class="patches">
-
  <div style="margin-bottom: 1rem;">
-
    <TabBar
-
      {options}
-
      on:select={e =>
-
        router.updateProjectRoute({
-
          search: `state=${e.detail}`,
-
        })}
-
      active={state} />
-
  </div>
-

-
  {#if filteredPatches.length}
-
    <div class="patches-list">
-
      {#each filteredPatches as patch}
-
        <!-- svelte-ignore a11y-click-events-have-key-events -->
-
        <div
-
          class="teaser"
-
          on:click={() => {
-
            router.updateProjectRoute({
-
              view: { resource: "patch", params: { patch: patch.id } },
-
            });
-
          }}>
-
          <PatchTeaser {patch} />
-
        </div>
-
      {/each}
-
    </div>
-
  {:else}
-
    <Placeholder emoji="🍂">
-
      <div slot="title">{capitalize(state)} patches</div>
-
      <div slot="body">No patches matched the current filter</div>
-
    </Placeholder>
-
  {/if}
-
</div>
modified src/views/projects/PeerSelector.svelte
@@ -21,24 +21,17 @@
  }[] = [];

  function createTitle(p: Peer): string {
-
    const name = p.person?.name ? p.person.name : p.id;
+
    const nodeId = formatNodeId(p.id);
    return p.delegate
-
      ? `${name} is a delegate of this project`
-
      : `${name} is a peer tracked by this seed`;
+
      ? `${nodeId} is a delegate of this project`
+
      : `${nodeId} is a peer tracked by this node`;
  }

  onMount(() => {
    meta = peers.find(p => p.id === peer);
    items = peers.map(p => {
-
      if (!p.person?.name) {
-
        console.debug("Not able to resolve peer identity for: ", p.id);
-
      }
-
      const key = p.person?.name
-
        ? `<span class="txt-bold">${p.person.name}</span> ${p.id}`
-
        : p.id;
-

      return {
-
        key,
+
        key: formatNodeId(p.id),
        value: p.id,
        title: createTitle(p),
        badge: p.delegate ? "delegate" : null,
@@ -91,7 +84,7 @@
      <Icon name="fork" />
      {#if meta}
        <span class="peer-id">
-
          {meta.person?.name ?? formatNodeId(meta.id)}
+
          {formatNodeId(meta.id)}
        </span>
        {#if meta.delegate}
          <Badge variant="primary">delegate</Badge>
modified src/views/projects/ProjectMeta.svelte
@@ -1,13 +1,13 @@
<script lang="ts">
-
  import type { PeerId, Project } from "@app/lib/project";
+
  import type { Project } from "@app/lib/project";

  import Clipboard from "@app/components/Clipboard.svelte";
  import DOMPurify from "dompurify";
  import ProjectLink from "@app/components/ProjectLink.svelte";
-
  import { formatSeedId } from "@app/lib/utils";
+
  import { formatNodeId } from "@app/lib/utils";

  export let project: Project;
-
  export let peer: PeerId | undefined = undefined;
+
  export let nodeId: string | undefined = undefined;

  const linkifyDescription = (text: string) => {
    return text.replaceAll(/(https?:\/\/[^\s]+)/g, `<a href="$1">$1</a>`);
@@ -27,7 +27,7 @@
    margin: 0 0.5rem;
    font-weight: var(--font-weight-normal);
  }
-
  .title .peer-id {
+
  .title .node-id {
    color: var(--color-foreground-5);
    font-weight: var(--font-weight-normal);
    display: flex;
@@ -87,11 +87,11 @@
        </span>
      </ProjectLink>
    </span>
-
    {#if peer}
-
      <span class="peer-id">
+
    {#if nodeId}
+
      <span class="node-id">
        <span class="divider">/</span>
-
        <span title={peer}>{formatSeedId(peer)}</span>
-
        <Clipboard text={peer} />
+
        <span title={nodeId}>{formatNodeId(nodeId)}</span>
+
        <Clipboard text={nodeId} />
      </span>
    {/if}
  </div>
modified src/views/projects/Tree.svelte
@@ -18,11 +18,11 @@
</script>

{#each tree.entries as entry (entry.path)}
-
  {#if window.HEARTWOOD ? entry.kind === "tree" : entry.info.objectType === "TREE"}
+
  {#if entry.kind === "tree"}
    <Folder
      {fetchTree}
      {loadingPath}
-
      name={window.HEARTWOOD ? entry.name : entry.info.name}
+
      name={entry.name}
      prefix={`${entry.path}/`}
      currentPath={path}
      on:select={onSelect} />
@@ -30,7 +30,7 @@
    <File
      active={entry.path === path}
      loading={entry.path === loadingPath}
-
      name={window.HEARTWOOD ? entry.name : entry.info.name}
+
      name={entry.name}
      on:click={() => onSelect({ detail: entry.path })} />
  {/if}
{/each}
modified src/views/projects/Tree/Folder.svelte
@@ -74,10 +74,10 @@
    {:then tree}
      {#if tree}
        {#each tree.entries as entry (entry.path)}
-
          {#if window.HEARTWOOD ? entry.kind === "tree" : entry.info.objectType === "TREE"}
+
          {#if entry.kind === "tree"}
            <svelte:self
              {fetchTree}
-
              name={window.HEARTWOOD ? entry.name : entry.info.name}
+
              name={entry.name}
              on:select={onSelectFile}
              prefix={`${entry.path}/`}
              {loadingPath}
@@ -86,7 +86,7 @@
            <File
              active={entry.path === currentPath}
              loading={entry.path === loadingPath}
-
              name={window.HEARTWOOD ? entry.name : entry.info.name}
+
              name={entry.name}
              on:click={() => {
                onSelectFile({ detail: entry.path });
              }} />
modified src/views/projects/View.svelte
@@ -1,10 +1,8 @@
<script lang="ts">
  import type { ProjectRoute } from "@app/lib/router/definitions";
  import type { State as IssueState } from "./Issues.svelte";
-
  import type { State as PatchState } from "./Patches.svelte";

  import * as issue from "@app/lib/issue";
-
  import * as patch from "@app/lib/patch";
  import * as proj from "@app/lib/project";
  import * as router from "@app/lib/router";
  import Loading from "@app/components/Loading.svelte";
@@ -20,8 +18,6 @@
  import Issues from "./Issues.svelte";
  import Message from "@app/components/Message.svelte";
  import NewIssue from "./Issue/New.svelte";
-
  import Patch from "./Patch.svelte";
-
  import Patches from "./Patches.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
  import ProjectMeta from "./ProjectMeta.svelte";

@@ -33,7 +29,6 @@

  $: searchParams = new URLSearchParams(activeRoute.params.search || "");
  $: issueFilter = (searchParams.get("state") as IssueState) || "open";
-
  $: patchFilter = (searchParams.get("state") as PatchState) || "proposed";

  const getProject = async (id: string, seed: string, peer?: string) => {
    const project = await proj.Project.get(id, seed, peer);
@@ -120,7 +115,7 @@
  </main>
{:then project}
  <main>
-
    <ProjectMeta {project} {peer} />
+
    <ProjectMeta {project} nodeId={peer} />
    {#await project.getRoot(revision)}
      <Loading center />
    {:then { tree, commit }}
@@ -182,26 +177,6 @@
            <Message error>{e.message}</Message>
          </div>
        {/await}
-
      {:else if activeRoute.params.view.resource === "patches"}
-
        {#await patch.Patch.getPatches(project.id, project.seed.addr)}
-
          <Loading center />
-
        {:then patches}
-
          <Patches {project} state={patchFilter} {patches} />
-
        {:catch e}
-
          <div class="message">
-
            <Message error>{e.message}</Message>
-
          </div>
-
        {/await}
-
      {:else if activeRoute.params.view.resource === "patch"}
-
        {#await patch.Patch.getPatch(project.id, activeRoute.params.view.params.patch, project.seed.addr)}
-
          <Loading center />
-
        {:then patch}
-
          <Patch {project} {patch} />
-
        {:catch e}
-
          <div class="message">
-
            <Message error>{e.message}</Message>
-
          </div>
-
        {/await}
      {:else}
        {unreachable(activeRoute.params.view)}
      {/if}
deleted src/views/seeds/Routes.svelte
@@ -1,11 +0,0 @@
-
<script lang="ts">
-
  import View from "@app/views/seeds/View.svelte";
-

-
  export let host: string;
-
</script>
-

-
{#if host === "radicle.local"}
-
  <View hostAndPort={"0.0.0.0"} />
-
{:else}
-
  <View hostAndPort={host} />
-
{/if}
modified src/views/seeds/View.svelte
@@ -1,25 +1,27 @@
<script lang="ts">
-
  import type { Host } from "@app/lib/api";
  import type { ProjectInfo } from "@app/lib/project";
  import type { Stats } from "@app/lib/seed";

+
  import { Project } from "@app/lib/project";
+
  import { Seed } from "@app/lib/seed";
+
  import { config } from "@app/lib/config";
+
  import {
+
    formatNodeId,
+
    formatSeedHost,
+
    twemoji,
+
    extractHost,
+
  } from "@app/lib/utils";
+

  import Clipboard from "@app/components/Clipboard.svelte";
  import Loading from "@app/components/Loading.svelte";
  import NotFound from "@app/components/NotFound.svelte";
  import Projects from "@app/views/seeds/View/Projects.svelte";
  import SeedAddress from "@app/views/seeds/View/SeedAddress.svelte";
-
  import { Project } from "@app/lib/project";
-
  import { Seed, defaultSeedPort } from "@app/lib/seed";
-
  import { formatSeedId, formatSeedHost, twemoji } from "@app/lib/utils";

  export let hostAndPort: string;

-
  const [host, port] = hostAndPort.includes(":")
-
    ? hostAndPort.split(":")
-
    : [hostAndPort, defaultSeedPort];
-

-
  const hostName = formatSeedHost(host);
-
  const seedHost: Host = { host, port: Number(port) };
+
  $: seedHost = extractHost(hostAndPort);
+
  $: hostName = formatSeedHost(seedHost.host);

  const getProjectsAndStats = async (
    seed: Seed,
@@ -28,7 +30,7 @@
    projects: ProjectInfo[];
  }> => {
    const stats = await seed.getStats();
-
    const projects = await Project.getProjects(seedHost, { perPage: 10 });
+
    const projects = await Project.getProjects(seed.addr, { perPage: 10 });
    return { stats, projects };
  };
</script>
@@ -81,7 +83,7 @@
  <title>{hostName}</title>
</svelte:head>

-
{#await Seed.lookup(host, Number(port))}
+
{#await Seed.lookup(seedHost)}
  <main class="layout-centered">
    <Loading center />
  </main>
@@ -99,17 +101,19 @@
    <div class="fields">
      <!-- Seed Address -->
      <div class="txt-highlight">Address</div>
-
      <SeedAddress {seed} port={seed.node.port} />
+
      <SeedAddress
+
        {seed}
+
        port={seed.node.port ?? config.seeds.defaultHttpdPort} />
      <!-- Seed ID -->
      <div class="txt-highlight">Seed ID</div>
      <div class="seed-label">
-
        {formatSeedId(seed.id)}
+
        {formatNodeId(seed.id)}
        <Clipboard small text={seed.id} />
      </div>
      <div class="layout-desktop" />
      <!-- API Port -->
      <div class="txt-highlight">API Port</div>
-
      <div>{port}</div>
+
      <div>{seed.addr.port}</div>
      <div class="layout-desktop" />
      <!-- API Version -->
      <div class="txt-highlight">Version</div>
@@ -133,7 +137,7 @@
{:catch}
  <div class="layout-centered">
    <NotFound
-
      title={host}
+
      title={seedHost.host}
      subtitle="Not able to query information from this seed." />
  </div>
{/await}
modified src/views/seeds/View/Projects.svelte
@@ -6,6 +6,7 @@
  import * as router from "@app/lib/router";
  import List from "@app/components/List.svelte";
  import Widget from "@app/views/projects/Widget.svelte";
+
  import { config } from "@app/lib/config";

  export let seed: Seed;
  export let projects: proj.ProjectInfo[];
@@ -39,9 +40,10 @@
      params: {
        view: { resource: "tree" },
        id: project.id,
-
        seed: seed.addr.port
-
          ? `${seed.addr.host}:${seed.addr.port}`
-
          : seed.addr.host,
+
        seed:
+
          seed.addr.port === config.seeds.defaultHttpdPort
+
            ? seed.addr.host
+
            : `${seed.addr.host}:${seed.addr.port}`,
        revision: project.head ?? undefined,
        hash: undefined,
        search: undefined,
modified tests/e2e/clipboard.spec.ts
@@ -3,7 +3,6 @@ import {
  expect,
  projectFixtureUrl,
  rid,
-
  ridPrefix,
  seedRemote,
  test,
} from "@tests/support/fixtures.js";
@@ -37,33 +36,43 @@ test("copy to clipboard", async ({ page, browserName, context }) => {
    const clipboardContent = await page.evaluate<string>(
      "navigator.clipboard.readText()",
    );
-
    expect(clipboardContent).toBe(`${ridPrefix}${rid}`);
+
    expect(clipboardContent).toBe(`${rid}`);
  }

  // `rad clone` URL.
  {
    await page.getByText("Clone").click();
-
    await page
-
      .locator(`text=rad clone rad://0.0.0.0/${rid.substring(0, 6)}`)
-
      .hover();
+
    await page.locator(`text=rad clone ${rid.substring(0, 6)}`).hover();
    await page
      .locator(".clone-url-wrapper > span")
      .first()
      .locator(".clipboard")
      .click();
-
    await expectClipboard(`rad clone rad://0.0.0.0/${rid}`, page);
+
    await expectClipboard(`rad clone ${rid}`, page);
  }

  // `git clone` URL.
  {
    await page.getByText("Clone").click();
-
    await page.locator(`text=https://0.0.0.0/${rid.substring(0, 6)}`).hover();
+
    await page
+
      .locator(
+
        `text=git clone https://0.0.0.0/${rid
+
          .replace("rad:", "")
+
          .substring(0, 10)}`,
+
      )
+
      .hover();
    await page
      .locator(".clone-url-wrapper > span")
      .last()
      .locator(".clipboard")
      .click();
-
    await expectClipboard(`https://0.0.0.0/${rid}.git`, page);
+
    await expectClipboard(
+
      `git clone https://0.0.0.0/${rid.replace(
+
        "rad:",
+
        "",
+
      )}.git source-browsing`,
+
      page,
+
    );
  }

  await page.goto("/seeds/radicle.local");
modified tests/e2e/project.spec.ts
@@ -6,7 +6,6 @@ import {
  expect,
  projectFixtureUrl,
  rid,
-
  ridPrefix,
  test,
} from "@tests/support/fixtures.js";
import { expectUrlPersistsReload } from "@tests/support/router";
@@ -29,7 +28,7 @@ test("navigate to project", async ({ page }) => {
  // Header.
  {
    const name = page.locator("text=source-browsing");
-
    const id = page.locator(`text=${ridPrefix}${rid}`);
+
    const id = page.locator(`text=${rid}`);
    const description = page.locator(
      "text=Git repository for source browsing tests",
    );
@@ -245,10 +244,10 @@ test("clone modal", async ({ page }) => {
  await page.goto(projectFixtureUrl);

  await page.getByText("Clone").click();
+
  await expect(page.locator(`text=rad clone ${rid}`)).toBeVisible();
  await expect(
-
    page.locator(`text=rad clone rad://0.0.0.0/${rid}`),
+
    page.locator(`text=https://0.0.0.0/${rid.replace("rad:", "")}.git`),
  ).toBeVisible();
-
  await expect(page.locator(`text=https://0.0.0.0/${rid}.git`)).toBeVisible();
});

test("peer and branch switching", async ({ page }) => {
@@ -257,18 +256,13 @@ test("peer and branch switching", async ({ page }) => {
  // Alice's peer.
  {
    await page.getByTitle("Change peer").click();
-
    if (process.env.HEARTWOOD) {
-
      await page.locator(`text=${aliceRemote}`).click();
-
      await expect(page.getByTitle("Change peer")).toHaveText(
-
        `did:key:${aliceRemote.substring(0, 6)}…${aliceRemote.slice(-6)}`,
-
      );
-
    } else {
-
      await page.locator("text=alice").click();
-
      await expect(page.getByTitle("Change peer")).toHaveText("alice delegate");
-
    }
+
    await page.locator(`text=${aliceRemote.substring(0, 6)}`).click();
+
    await expect(page.getByTitle("Change peer")).toHaveText(
+
      `did:key:${aliceRemote.substring(0, 6)}…${aliceRemote.slice(-6)}`,
+
    );
    await expect(
      page.locator(
-
        `text=source-browsing / ${aliceRemote.substring(
+
        `text=source-browsing / did:key:${aliceRemote.substring(
          0,
          6,
        )}…${aliceRemote.slice(-6)}`,
@@ -326,32 +320,19 @@ test("peer and branch switching", async ({ page }) => {
  // Bob's peer.
  {
    await page.getByTitle("Change peer").click();
-
    if (process.env.HEARTWOOD) {
-
      await page.locator(`text=${bobRemote}`).click();
-
      await expect(page.getByTitle("Change peer")).toHaveText(
-
        `did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
-
      );
-
    } else {
-
      await page.locator("text=bob").click();
-
      await expect(page.getByTitle("Change peer")).toHaveText("bob");
-
    }
+
    await page.locator(`text=${bobRemote.substring(0, 6)}`).click();
+
    await expect(page.getByTitle("Change peer")).toHaveText(
+
      `did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
+
    );
    await expect(page.getByTitle("Change peer")).not.toHaveText("delegate");

    // Default `main` branch.
    {
-
      if (process.env.HEARTWOOD) {
-
        await expect(page.getByTitle("Current branch")).toContainText(
-
          "main 1e0bb83",
-
        );
-
        await expectCounts({ commits: 9, contributors: 2 }, page);
-
        await expect(page.locator("text=1e0bb83 Update readme")).toBeVisible();
-
      } else {
-
        await expect(page.getByTitle("Current branch")).toContainText(
-
          "main 2b32f6f",
-
        );
-
        await expectCounts({ commits: 9, contributors: 2 }, page);
-
        await expect(page.locator("text=2b32f6f Update readme")).toBeVisible();
-
      }
+
      await expect(page.getByTitle("Current branch")).toContainText(
+
        "main 1e0bb83",
+
      );
+
      await expectCounts({ commits: 9, contributors: 2 }, page);
+
      await expect(page.locator("text=1e0bb83 Update readme")).toBeVisible();
    }
  }
});
@@ -360,11 +341,7 @@ test("only one modal can be open at a time", async ({ page }) => {
  await page.goto(projectFixtureUrl);

  await page.getByTitle("Change peer").click();
-
  if (process.env.HEARTWOOD) {
-
    await page.locator(`text=${aliceRemote.substring(0, 6)}`).click();
-
  } else {
-
    await page.locator(`text=alice ${aliceRemote.substring(0, 6)}`).click();
-
  }
+
  await page.locator(`text=${aliceRemote.substring(0, 6)}`).click();

  await page.getByText("Clone").click();
  await expect(page.locator("text=Code font")).not.toBeVisible();
@@ -381,11 +358,7 @@ test("only one modal can be open at a time", async ({ page }) => {
  await page.getByTitle("Change peer").click();
  await expect(page.locator("text=Code font")).not.toBeVisible();
  await expect(page.locator("text=Use the Radicle CLI")).not.toBeVisible();
-
  if (process.env.HEARTWOOD) {
-
    await expect(page.locator(`text=${bobRemote}`)).toBeVisible();
-
  } else {
-
    await expect(page.locator("text=bob hyyzz9")).toBeVisible();
-
  }
+
  await expect(page.locator(`text=${bobRemote.substring(0, 6)}`)).toBeVisible();
  await expect(page.locator("text=feature/branch")).not.toBeVisible();

  await page.locator('button[name="Settings"]').click();
@@ -398,7 +371,7 @@ test("only one modal can be open at a time", async ({ page }) => {
test.describe("browser error handling", () => {
  test("error appears when folder can't be loaded", async ({ page }) => {
    await page.route(
-
      `**/v1/projects/${ridPrefix}${rid}/tree/${aliceMainHead}/markdown/`,
+
      `**/v1/projects/${rid}/tree/${aliceMainHead}/markdown/`,
      route => route.fulfill({ status: 500 }),
    );

@@ -413,7 +386,7 @@ test.describe("browser error handling", () => {
  });
  test("error appears when file can't be loaded", async ({ page }) => {
    await page.route(
-
      `**/v1/projects/${ridPrefix}${rid}/blob/${aliceMainHead}/.hidden`,
+
      `**/v1/projects/${rid}/blob/${aliceMainHead}/.hidden`,
      route => route.fulfill({ status: 500 }),
    );

@@ -423,9 +396,8 @@ test.describe("browser error handling", () => {
    await expect(page.locator("text=Not able to load file")).toBeVisible();
  });
  test("error appears when README can't be loaded", async ({ page }) => {
-
    await page.route(
-
      `**/v1/projects/${ridPrefix}${rid}/readme/${aliceMainHead}`,
-
      route => route.fulfill({ status: 500 }),
+
    await page.route(`**/v1/projects/${rid}/readme/${aliceMainHead}`, route =>
+
      route.fulfill({ status: 500 }),
    );

    await page.goto(projectFixtureUrl);
@@ -435,7 +407,7 @@ test.describe("browser error handling", () => {
  });
  test("error appears when navigating to missing file", async ({ page }) => {
    await page.route(
-
      `**/v1/projects/${ridPrefix}${rid}/blob/${aliceMainHead}/.hidden`,
+
      `**/v1/projects/${rid}/blob/${aliceMainHead}/.hidden`,
      route => route.fulfill({ status: 500 }),
    );

@@ -447,7 +419,7 @@ test.describe("browser error handling", () => {
    page,
  }) => {
    await page.route(
-
      `**/v1/projects/${ridPrefix}${rid}/blob/${aliceMainHead}/src/black-square.png`,
+
      `**/v1/projects/${rid}/blob/${aliceMainHead}/src/black-square.png`,
      route => route.fulfill({ status: 404 }),
    );

modified tests/e2e/project/commit.spec.ts
@@ -5,18 +5,12 @@ import {
  bobRemote,
} from "@tests/support/fixtures.js";

-
const modifiedFileFixture = `${projectFixtureUrl}/remotes/${bobRemote}/commits/${
-
  process.env.HEARTWOOD
-
    ? "1e0bb83a89b63da815f2fc24e7ae3c5ceb30e0eb"
-
    : "2b32f6fe50090ebdb4cd7441e943330da3e6ff04"
-
}`;
+
const modifiedFileFixture = `${projectFixtureUrl}/remotes/${bobRemote}/commits/${"1e0bb83a89b63da815f2fc24e7ae3c5ceb30e0eb"}`;

test("navigation from commit list", async ({ page }) => {
  await page.goto(projectFixtureUrl);
  await page.getByTitle("Change peer").click();
-
  await page
-
    .locator(process.env.HEARTWOOD ? `text=${bobRemote}` : "text=bob hyyzz9")
-
    .click();
+
  await page.locator(`text=${bobRemote.substring(0, 6)}`).click();
  await page.locator('role=button[name="Commit count"]').click();

  await page.locator("text=Update readme").click();
@@ -35,13 +29,7 @@ test("relative timestamps", async ({ page }) => {
  });
  await page.goto(modifiedFileFixture);
  await expect(
-
    page.locator(
-
      `.commit header >> text=${
-
        process.env.HEARTWOOD
-
          ? "Bob Belcher committed now"
-
          : "bob committed 22 hours ago"
-
      }`,
-
    ),
+
    page.locator(`.commit header >> text=${"Bob Belcher committed now"}`),
  ).toBeVisible();
});

@@ -52,16 +40,9 @@ test("modified file", async ({ page }) => {
  {
    const header = page.locator(".commit header");
    await expect(header.locator("text=Update readme")).toBeVisible();
-
    if (!process.env.HEARTWOOD) {
-
      await expect(header.locator("text=Verified")).toBeVisible();
-
      await expect(
-
        header.locator("text=2b32f6fe50090ebdb4cd7441e943330da3e6ff04"),
-
      ).toBeVisible();
-
    } else {
-
      await expect(
-
        header.locator("text=1e0bb83a89b63da815f2fc24e7ae3c5ceb30e0eb"),
-
      ).toBeVisible();
-
    }
+
    await expect(
+
      header.locator("text=1e0bb83a89b63da815f2fc24e7ae3c5ceb30e0eb"),
+
    ).toBeVisible();
  }

  // Diff header.
modified tests/e2e/project/commits.spec.ts
@@ -13,15 +13,10 @@ test("peer and branch switching", async ({ page }) => {
  // Alice's peer.
  {
    await page.getByTitle("Change peer").click();
-
    if (process.env.HEARTWOOD) {
-
      await page.locator(`text=${aliceRemote}`).click();
-
      await expect(page.getByTitle("Change peer")).toHaveText(
-
        `did:key:${aliceRemote.substring(0, 6)}…${aliceRemote.slice(-6)}`,
-
      );
-
    } else {
-
      await page.locator("text=alice hybg18").click();
-
      await expect(page.getByTitle("Change peer")).toHaveText("alice delegate");
-
    }
+
    await page.locator(`text=${aliceRemote.substring(0, 6)}`).click();
+
    await expect(page.getByTitle("Change peer")).toHaveText(
+
      `did:key:${aliceRemote.substring(0, 6)}…${aliceRemote.slice(-6)}`,
+
    );

    await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
    await expect(
@@ -66,20 +61,15 @@ test("peer and branch switching", async ({ page }) => {
  // Bob's peer.
  {
    await page.getByTitle("Change peer").click();
-
    if (process.env.HEARTWOOD) {
-
      await page.locator(`text=${bobRemote}`).click();
-
      await expect(page.getByTitle("Change peer")).toHaveText(
-
        `did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
-
      );
-
    } else {
-
      await page.locator("text=bob hyyzz9").click();
-
      await expect(page.getByTitle("Change peer")).toHaveText("bob");
-
    }
+
    await page.locator(`text=${bobRemote.substring(0, 6)}`).click();
+
    await expect(page.getByTitle("Change peer")).toHaveText(
+
      `did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
+
    );

    await expect(page.getByText("Tuesday, December 20, 2022")).toBeVisible();
    await expect(
      page.locator(".commit-group-headers").first().locator(".commit-teaser"),
-
    ).toHaveCount(process.env.HEARTWOOD ? 1 : 3);
+
    ).toHaveCount(1);

    await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
    await expect(
@@ -88,13 +78,8 @@ test("peer and branch switching", async ({ page }) => {

    await page.pause();
    const latestCommit = page.locator(".commit-teaser").first();
-
    if (process.env.HEARTWOOD) {
-
      await expect(latestCommit).toContainText("Update readme");
-
      await expect(latestCommit).toContainText("1e0bb83");
-
    } else {
-
      await expect(latestCommit).toContainText("Update readme");
-
      await expect(latestCommit).toContainText("2b32f6f");
-
    }
+
    await expect(latestCommit).toContainText("Update readme");
+
    await expect(latestCommit).toContainText("1e0bb83");

    const earliestCommit = page.locator(".commit-teaser").last();
    await expect(earliestCommit).toContainText(
@@ -104,30 +89,6 @@ test("peer and branch switching", async ({ page }) => {
  }
});

-
test("verified badge", async ({ page }) => {
-
  await page.goto(projectFixtureUrl);
-
  await page.locator('role=button[name="Commit count"]').click();
-

-
  await page.getByTitle("Change peer").click();
-
  if (process.env.HEARTWOOD) {
-
    await page.locator(`text=${bobRemote}`).click();
-
    await expect(page.getByTitle("Change peer")).toHaveText(
-
      `did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
-
    );
-
  } else {
-
    await page.locator("text=bob hyyzz9").click();
-
    await expect(page.getByTitle("Change peer")).toHaveText("bob");
-
    // not applicable to heartwood?
-
    await page.locator("text=Verified").hover();
-
    await expect(
-
      page.locator(
-
        "text=This commit was signed with the committer's radicle key.",
-
      ),
-
    ).toBeVisible();
-
    await expect(page.locator(`text=bob committed ${bobRemote}`)).toBeVisible();
-
  }
-
});
-

test("relative timestamps", async ({ page }) => {
  await page.addInitScript(() => {
    window.initializeTestStubs = () => {
@@ -143,22 +104,13 @@ test("relative timestamps", async ({ page }) => {
  await page.locator('role=button[name="Commit count"]').click();

  await page.getByTitle("Change peer").click();
-
  if (process.env.HEARTWOOD) {
-
    await page.locator(`text=${bobRemote}`).click();
-
    await expect(page.getByTitle("Change peer")).toHaveText(
-
      `did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
-
    );
-
    const latestCommit = page.locator(".commit-teaser").first();
-
    await expect(latestCommit).toContainText("Bob Belcher committed now");
-
    await expect(latestCommit).toContainText("1e0bb83");
-
  } else {
-
    await page.locator("text=bob hyyzz9").click();
-
    await expect(page.getByTitle("Change peer")).toHaveText("bob");
-
    const latestCommit = page.locator(".commit-teaser").first();
-
    await expect(latestCommit).toContainText("bob committed 22 hours ago");
-
    await expect(latestCommit).toContainText("2b32f6f");
-
  }
-

+
  await page.locator(`text=${bobRemote.substring(0, 6)}`).click();
+
  await expect(page.getByTitle("Change peer")).toHaveText(
+
    `did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
+
  );
+
  const latestCommit = page.locator(".commit-teaser").first();
+
  await expect(latestCommit).toContainText("Bob Belcher committed now");
+
  await expect(latestCommit).toContainText("1e0bb83");
  const earliestCommit = page.locator(".commit-teaser").last();
  await expect(earliestCommit).toContainText(
    "Alice Liddell committed last month",
modified tests/e2e/search.spec.ts
@@ -2,7 +2,6 @@ import {
  test,
  expect,
  rid,
-
  ridPrefix,
  projectFixtureUrl,
} from "@tests/support/fixtures.js";

@@ -10,11 +9,11 @@ test("navigate to existing project", async ({ page }) => {
  await page.goto("/");
  const searchInput = page.getByPlaceholder("Search a name…");
  await searchInput.click();
-
  await searchInput.fill(`${ridPrefix}${rid}`);
+
  await searchInput.fill(`${rid}`);
  await searchInput.press("Enter");

  await expect(page).toHaveURL(`${projectFixtureUrl}/tree`);
-
  await expect(searchInput).not.toHaveValue(`${ridPrefix}${rid}`);
+
  await expect(searchInput).not.toHaveValue(`${rid}`);
});

test("navigate to a project that does not exist", async ({ page }) => {
@@ -22,7 +21,7 @@ test("navigate to a project that does not exist", async ({ page }) => {
  const searchInput = page.getByPlaceholder("Search a name…");
  await searchInput.click();

-
  const nonExistantId = `${ridPrefix}:zt${rid.substring(2)}`;
+
  const nonExistantId = "rad:zAAAAAAAAAAAAAAAAAAAAAAAAAAA";
  await searchInput.fill(nonExistantId);
  await searchInput.press("Enter");

modified tests/e2e/seed.spec.ts
@@ -2,7 +2,6 @@ import {
  aliceMainHead,
  expect,
  rid,
-
  ridPrefix,
  seedPort,
  seedRemote,
  seedVersion,
@@ -44,8 +43,8 @@ test("seed projects", async ({ page }) => {

  // Show project ID on hover.
  {
-
    await expect(project.locator(`text=${ridPrefix}${rid}`)).not.toBeVisible();
+
    await expect(project.locator(`text=${rid}`)).not.toBeVisible();
    await project.hover();
-
    await expect(project.locator(`text=${ridPrefix}${rid}`)).toBeVisible();
+
    await expect(project.locator(`text=${rid}`)).toBeVisible();
  }
});
deleted tests/fixtures/seeds/palm-heartwood.tar.bz2
modified tests/fixtures/seeds/palm.tar.bz2
modified tests/support/fixtures.ts
@@ -49,6 +49,9 @@ export const test = base.extend<{
          window.APP_CONFIG = {
            reactions: [],
            seeds: {
+
              defaultHttpdPort: 8080,
+
              defaultHttpdScheme: "http",
+
              defaultNodePort: 8776,
              pinned: [{ host: "0.0.0.0", emoji: "🚀" }],
            },
            projects: { pinned: [] },
@@ -124,14 +127,13 @@ function log(text: string, label: string, outputLog: Stream.Writable) {
  }
}

-
export const appConfigWithFixture = process.env.HEARTWOOD
-
  ? configFixtureHeartwood
-
  : configFixture;
-

export function configFixture() {
  window.APP_CONFIG = {
    reactions: [],
    seeds: {
+
      defaultHttpdPort: 8080,
+
      defaultHttpdScheme: "http",
+
      defaultNodePort: 8776,
      pinned: [{ host: "0.0.0.0", emoji: "🚀" }],
    },
    projects: {
@@ -146,10 +148,13 @@ export function configFixture() {
  };
}

-
export function configFixtureHeartwood() {
+
export function appConfigWithFixture() {
  window.APP_CONFIG = {
    reactions: [],
    seeds: {
+
      defaultHttpdPort: 8080,
+
      defaultHttpdScheme: "http",
+
      defaultNodePort: 8776,
      pinned: [{ host: "0.0.0.0", emoji: "🚀" }],
    },
    projects: {
@@ -165,19 +170,10 @@ export function configFixtureHeartwood() {
}

export const aliceMainHead = "fcc929424b82984b7cbff9c01d2e20d9b1249842";
-
export const aliceRemote = process.env.HEARTWOOD
-
  ? "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
-
  : "hybg18bc4cu8z9xtj44skxperfdpxpp1wp8zygyzti5kfiggdizfxy";
-
export const bobRemote = process.env.HEARTWOOD
-
  ? "z6MksMTThc1aDU2Ztc43jJUivuyBLNWiLsDf4X65rABe7HbA"
-
  : "hyyzz9w4ffg16zftjki3enajm4mkqkayb5ch1p6ns3f83np1hqkrp6";
-
export const rid = process.env.HEARTWOOD
-
  ? "zKtT7DmF9H34KkvcKj9PHW19WzjT"
-
  : "hnrkdi8be7n4hhqoz9rpzrgd68u9dr3zsxgmy";
-
export const ridPrefix = process.env.HEARTWOOD ? "rad:" : "rad:git:";
-
export const projectFixtureUrl = `/seeds/0.0.0.0/${ridPrefix}${rid}`;
-
export const seedPort = process.env.HEARTWOOD ? 8080 : "8777";
-
export const seedVersion = process.env.HEARTWOOD ? "0.1.0" : "0.2.0";
-
export const seedRemote = process.env.HEARTWOOD
-
  ? "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"
-
  : "hybuytx44z9cfsm5739wecia9j4b7expgc15qkazph59szp57m4d3o";
+
export const aliceRemote = "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi";
+
export const bobRemote = "z6MksMTThc1aDU2Ztc43jJUivuyBLNWiLsDf4X65rABe7HbA";
+
export const rid = "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT";
+
export const projectFixtureUrl = `/seeds/0.0.0.0/${rid}`;
+
export const seedPort = 8080;
+
export const seedVersion = "0.1.0";
+
export const seedRemote = "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8";
modified tests/support/globalSetup.ts
@@ -11,28 +11,20 @@ export default async function globalSetup(_config: FullConfig): Promise<void> {
async function assertHttpApiRunning(): Promise<void> {
  const notRunningMessage =
    "The http-api server with test fixtures needs to be running.\n" +
-
    (process.env.HEARTWOOD
-
      ? "👉 You can start it with `./scripts/run-httpd-with-fixtures`\n"
-
      : "👉 You can start it with `./scripts/run-http-api-with-fixtures`\n");
+
    "👉 You can start it with `./scripts/run-httpd-with-fixtures`\n";

-
  let peerId: string | undefined = undefined;
+
  let nodeId: string | undefined = undefined;

  try {
-
    if (process.env.HEARTWOOD) {
-
      const response = await fetch(`http://0.0.0.0:${seedPort}/api`);
-
      const data = await response.json();
-
      peerId = data.node.id;
-
    } else {
-
      const response = await fetch(`http://0.0.0.0:${seedPort}`);
-
      const data = await response.json();
-
      peerId = data.peer.id;
-
    }
+
    const response = await fetch(`http://0.0.0.0:${seedPort}/api`);
+
    const data = await response.json();
+
    nodeId = data.node.id;
  } catch (err) {
    console.error(err);
    throw new Error(notRunningMessage);
  }

-
  if (peerId !== seedRemote) {
+
  if (nodeId !== seedRemote) {
    const wrongSeedMessage = `The server on port ${seedPort} doesn't have the right fixtures.\n`;
    throw new Error(wrongSeedMessage + notRunningMessage);
  }
modified tests/unit/utils.test.ts
@@ -11,6 +11,15 @@ describe("Format functions", () => {

  test.each([
    {
+
      id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
+
      expected: "rad:zKtT7D…19WzjT",
+
    },
+
  ])("formatRepositoryId $id => $expected", ({ id, expected }) => {
+
    expect(utils.formatRepositoryId(id)).toEqual(expected);
+
  });
+

+
  test.each([
+
    {
      id: "did:key:z6MkmzRwg47UWQxczLLLFfkEwpBGitjzJ1vKPE8U9ymd6fz6",
      expected: "did:key:z6Mkmz…md6fz6",
    },
@@ -22,22 +31,17 @@ describe("Format functions", () => {
    expect(utils.formatNodeId(id)).toEqual(expected);
  });

-
  test("formatRadicleId", () => {
-
    if (process.env.HEARTWOOD) {
-
      expect(utils.formatRadicleId("rad:zKtT7DmF9H34KkvcKj9PHW19WzjT")).toEqual(
-
        "rad:zKtT7D…19WzjT",
-
      );
-
    } else {
-
      expect(
-
        utils.formatRadicleId("rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o"),
-
      ).toEqual("rad:git:hnrkem…d84c9o");
-
    }
-
  });
-

-
  test("formatRadicleId throw when wrong ID", () => {
-
    expect(() =>
-
      utils.formatRadicleId("hnrkemobagsicpf9sr95o3g551otspcd84c9o"),
-
    ).toThrow();
+
  test.each([
+
    {
+
      id: "rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5",
+
      expected: "rad:z4V1sj…p4DtH5",
+
    },
+
    {
+
      id: "z4V1sjrXqjvFdnCUbxPFqd5p4DtH5",
+
      expected: "rad:z4V1sj…p4DtH5",
+
    },
+
  ])("formatRepositoryId $id => $expected", ({ id, expected }) => {
+
    expect(utils.formatRepositoryId(id)).toEqual(expected);
  });

  test.each([
@@ -67,19 +71,40 @@ describe("String Assertions", () => {
  });

  test.each([
-
    process.env.HEARTWOOD
-
      ? { id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", expected: true }
-
      : { id: "rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o", expected: true },
+
    { id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", expected: true },
+
    { id: "z2H9aHDurxd8Uvx2jsvW4e5mamy5S", expected: true },
+
    { id: "rad:BBBBBBBBBBBBBBBBBBBBBBBBBBBB", expected: false },
    { id: "0x1234567890123456789012345678901234567890", expected: false },
-
  ])("isRadicleId $id => $expected", ({ id, expected }) => {
-
    expect(utils.isRadicleId(id)).toEqual(expected);
+
    {
+
      id: "did:key:z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx",
+
      expected: false,
+
    },
+
    {
+
      id: "z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx",
+
      expected: false,
+
    },
+
  ])("isRepositoryId $id => $expected", ({ id, expected }) => {
+
    expect(utils.isRepositoryId(id)).toEqual(expected);
  });

  test.each([
-
    { id: "hnrkj4c35uoyceb3d1dsscx8qq55cikrd1aio", expected: true },
+
    {
+
      id: "did:key:z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx",
+
      expected: true,
+
    },
+
    {
+
      id: "z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx",
+
      expected: true,
+
    },
+
    {
+
      id: "did:key:CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
+
      expected: false,
+
    },
+
    { id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", expected: false },
+
    { id: "z2H9aHDurxd8Uvx2jsvW4e5mamy5S", expected: false },
    { id: "0x1234567890123456789012345678901234567890", expected: false },
-
  ])("isPeerId $id => $expected", ({ id, expected }) => {
-
    expect(utils.isPeerId(id)).toEqual(expected);
+
  ])("isNodeId $id => $expected", ({ id, expected }) => {
+
    expect(utils.isNodeId(id)).toEqual(expected);
  });

  test.each([
@@ -131,8 +156,8 @@ describe("Parse Functions", () => {
        pubkey: "z6MkmzRwg47UWQxczLLLFfkEwpBGitjzJ1vKPE8U9ymd6fz6",
      },
    },
-
  ])("parseNid", ({ input, expected }) => {
-
    expect(utils.parseNid(input)).toEqual(expected);
+
  ])("parseNodeId", ({ input, expected }) => {
+
    expect(utils.parseNodeId(input)).toEqual(expected);
  });
});

modified vite.config.ts
@@ -12,7 +12,6 @@ function defineConstants() {
  const constants = {
    VITEST: process.env.VITEST !== undefined,
    PLAYWRIGHT: process.env.PLAYWRIGHT_TEST_BASE_URL !== undefined,
-
    HEARTWOOD: process.env.HEARTWOOD !== undefined,
  };

  // Don't overwrite HASH_ROUTING in Playwright tests, so we can control it