Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Improve landing page performance
Merged rudolfs opened 1 day ago

See individual commits for details.

check check-visual check-unit-test check-http-client-unit-test check-radicle-httpd check-e2e check-build check-http ๐Ÿ‘‰ Workflow runs ๐Ÿ‘‰ Branch on GitHub

5 files changed +208 -32 ad8b0699 โ†’ f472fe89
modified src/components/RepoCard.svelte
@@ -69,6 +69,40 @@
    align-self: flex-end;
  }

+
  .activity-shimmer {
+
    width: 100%;
+
    min-width: 185px;
+
    aspect-ratio: 493 / 116;
+
    background: linear-gradient(
+
      90deg,
+
      var(--color-surface-base) 0%,
+
      var(--color-surface-subtle) 50%,
+
      var(--color-surface-base) 100%
+
    );
+
    background-size: 200% 100%;
+
    animation:
+
      shimmer 1.8s ease-in-out infinite,
+
      shimmer-fade-in 200ms ease-out 500ms both;
+
  }
+

+
  @keyframes shimmer {
+
    0% {
+
      background-position: 100% 0;
+
    }
+
    100% {
+
      background-position: -100% 0;
+
    }
+
  }
+

+
  @keyframes shimmer-fade-in {
+
    from {
+
      opacity: 0;
+
    }
+
    to {
+
      opacity: 1;
+
    }
+
  }
+

  .headline-and-badges {
    display: flex;
    gap: 0.5rem;
@@ -156,11 +190,15 @@
        </span>
      </div>
      <div class="activity">
-
        <ActivityDiagram
-
          id={repo.rid}
-
          viewBoxHeight={100}
-
          styleColor="var(--color-text-brand)"
-
          activity={repoInfo.activity} />
+
        {#await repoInfo.activity}
+
          <div class="activity-shimmer"></div>
+
        {:then activity}
+
          <ActivityDiagram
+
            id={repo.rid}
+
            viewBoxHeight={100}
+
            styleColor="var(--color-text-brand)"
+
            {activity} />
+
        {/await}
      </div>
    </div>
    <div>
modified src/components/RepoCard.ts
@@ -6,13 +6,14 @@ import { HttpdClient } from "@http-client";
export interface RepoInfo {
  repo: Repo;
  baseUrl: BaseUrl;
-
  activity: WeeklyActivity[];
+
  activity: Promise<WeeklyActivity[]>;
}

export async function fetchRepoInfos(
  baseUrl: BaseUrl,
  query?: RepoListQuery,
  delegate?: string,
+
  activitySignal?: AbortSignal,
): Promise<RepoInfo[]> {
  const api = new HttpdClient(baseUrl);
  let repos: Repo[];
@@ -22,24 +23,33 @@ export async function fetchRepoInfos(
  } else {
    repos = await api.repo.getAll(query);
  }
-
  const info = await Promise.all(
-
    repos
-
      .filter(r => Boolean(r.payloads["xyz.radicle.project"]))
-
      .map(async repo => {
-
        const activity = await loadRepoActivity(repo.rid, baseUrl);
-
        return { repo, activity, baseUrl };
+

+
  return repos
+
    .filter(r => Boolean(r.payloads["xyz.radicle.project"]))
+
    .map(repo => ({
+
      repo,
+
      baseUrl,
+
      activity: loadRepoActivity(repo.rid, baseUrl, activitySignal).catch(e => {
+
        if (import.meta.env.DEV && (e as Error)?.name !== "AbortError") {
+
          console.warn("loadRepoActivity failed for", repo.rid, e);
+
        }
+
        return [];
      }),
-
  );
+
    }));
+
}

-
  return info.sort((a, b) => {
-
    if (a.activity.length === 0 && b.activity.length === 0) {
-
      return 0;
-
    } else if (a.activity.length === 0 && b.activity.length > 0) {
-
      return 1;
-
    } else if (b.activity.length === 0 && a.activity.length > 0) {
-
      return -1;
-
    } else {
+
export async function sortRepoInfosByActivity(
+
  repos: RepoInfo[],
+
): Promise<RepoInfo[]> {
+
  const withActivity = await Promise.all(
+
    repos.map(async r => ({ info: r, activity: await r.activity })),
+
  );
+
  return withActivity
+
    .sort((a, b) => {
+
      if (a.activity.length === 0 && b.activity.length === 0) return 0;
+
      if (a.activity.length === 0) return 1;
+
      if (b.activity.length === 0) return -1;
      return b.activity[0].time - a.activity[0].time;
-
    }
-
  });
+
    })
+
    .map(x => x.info);
}
modified src/lib/commit.ts
@@ -110,9 +110,15 @@ function groupCommitsByWeek(commits: number[]): WeeklyActivity[] {
  return groupedCommits;
}

-
export async function loadRepoActivity(id: string, baseUrl: BaseUrl) {
+
export async function loadRepoActivity(
+
  id: string,
+
  baseUrl: BaseUrl,
+
  signal?: AbortSignal,
+
) {
  const api = new HttpdClient(baseUrl);
-
  const commits = await api.repo.getActivity(id);
+
  const timeout = AbortSignal.timeout(8000);
+
  const abort = signal ? AbortSignal.any([signal, timeout]) : timeout;
+
  const commits = await api.repo.getActivity(id, { abort });

  return groupCommitsByWeek(commits.activity);
}
modified src/views/nodes/ReposView.svelte
@@ -1,8 +1,14 @@
<script lang="ts">
  import type { BaseUrl, NodeStats } from "@http-client";
+
  import type { RepoInfo } from "@app/components/RepoCard";
+

+
  import { onDestroy } from "svelte";

  import * as router from "@app/lib/router";
-
  import { fetchRepoInfos } from "@app/components/RepoCard";
+
  import {
+
    fetchRepoInfos,
+
    sortRepoInfosByActivity,
+
  } from "@app/components/RepoCard";
  import { handleError } from "@app/views/nodes/error";

  import Loading from "@app/components/Loading.svelte";
@@ -15,12 +21,26 @@
  let listState: "pinned" | "all" = "pinned";
  let page = 0;
  let hasPinnedRepos = true;
+
  let sortByActivity = false;
+
  let sorting = false;
+
  let displayedRepos: RepoInfo[] = [];
+

+
  let activityAbort: AbortController | undefined;
+

+
  function newActivitySession(): AbortSignal {
+
    activityAbort?.abort();
+
    activityAbort = new AbortController();
+
    return activityAbort.signal;
+
  }
+

+
  onDestroy(() => activityAbort?.abort());

  // Reset state when baseUrl changes
  $: if (baseUrl) {
    listState = "pinned";
    page = 0;
    hasPinnedRepos = true;
+
    sortByActivity = false;
  }

  $: perPage = listState === "pinned" ? stats.repos.total : 24;
@@ -29,11 +49,13 @@
  function showPinned() {
    listState = "pinned";
    page = 0;
+
    sortByActivity = false;
  }

  function showAll() {
    listState = "all";
    page = 0;
+
    sortByActivity = false;
  }

  async function fetchRepos(
@@ -41,7 +63,12 @@
    perPage: number,
    page: number,
  ) {
-
    const repos = await fetchRepoInfos(baseUrl, { show, perPage, page });
+
    const repos = await fetchRepoInfos(
+
      baseUrl,
+
      { show, perPage, page },
+
      undefined,
+
      newActivitySession(),
+
    );

    if (
      hasPinnedRepos &&
@@ -54,8 +81,25 @@
      return [];
    }

+
    sortByActivity = false;
+
    displayedRepos = repos;
    return repos;
  }
+

+
  async function toggleSortByActivity(repos: RepoInfo[]) {
+
    if (sortByActivity) {
+
      sortByActivity = false;
+
      displayedRepos = repos;
+
      return;
+
    }
+
    sorting = true;
+
    try {
+
      displayedRepos = await sortRepoInfosByActivity(repos);
+
      sortByActivity = true;
+
    } finally {
+
      sorting = false;
+
    }
+
  }
</script>

<style>
@@ -127,7 +171,7 @@
  {:then repoInfos}
    {#if repoInfos.length > 0}
      <div class="repo-grid">
-
        {#each repoInfos as repoInfo}
+
        {#each displayedRepos as repoInfo (repoInfo.repo.rid)}
          <RepoCard {baseUrl} {repoInfo} />
        {/each}
      </div>
@@ -136,6 +180,13 @@
          <div class="subtitle">
            {repoInfos.length}
            pinned {repoInfos.length === 1 ? "repository" : "repositories"} ยท
+
            <button
+
              class="text-button"
+
              disabled={sorting}
+
              on:click={() => toggleSortByActivity(repoInfos)}>
+
              {sortByActivity ? "Default order" : "Sort by activity"}
+
            </button>
+
            ยท
            <button class="text-button" on:click={showAll}>Browse all</button>
          </div>
        {:else}
modified src/views/users/UserReposView.svelte
@@ -1,9 +1,15 @@
<script lang="ts">
  import type { BaseUrl, NodeIdentity, NodeStats } from "@http-client";
+
  import type { RepoInfo } from "@app/components/RepoCard";
+

+
  import { onDestroy } from "svelte";

  import * as router from "@app/lib/router";
  import * as utils from "@app/lib/utils";
-
  import { fetchRepoInfos } from "@app/components/RepoCard";
+
  import {
+
    fetchRepoInfos,
+
    sortRepoInfosByActivity,
+
  } from "@app/components/RepoCard";
  import { handleError } from "@app/views/nodes/error";

  import Badge from "@app/components/Badge.svelte";
@@ -16,6 +22,51 @@
  export let stats: NodeStats;
  export let user: NodeIdentity;
  export let did: { prefix: string; pubkey: string };
+

+
  let sortByActivity = false;
+
  let sorting = false;
+
  let displayedRepos: RepoInfo[] = [];
+

+
  let activityAbort: AbortController | undefined;
+

+
  function newActivitySession(): AbortSignal {
+
    activityAbort?.abort();
+
    activityAbort = new AbortController();
+
    return activityAbort.signal;
+
  }
+

+
  onDestroy(() => activityAbort?.abort());
+

+
  $: if (baseUrl || did) {
+
    sortByActivity = false;
+
  }
+

+
  async function fetchRepos() {
+
    const repos = await fetchRepoInfos(
+
      baseUrl,
+
      { show: "all", perPage: stats.repos.total },
+
      utils.formatDid(did),
+
      newActivitySession(),
+
    );
+
    sortByActivity = false;
+
    displayedRepos = repos;
+
    return repos;
+
  }
+

+
  async function toggleSortByActivity(repos: RepoInfo[]) {
+
    if (sortByActivity) {
+
      sortByActivity = false;
+
      displayedRepos = repos;
+
      return;
+
    }
+
    sorting = true;
+
    try {
+
      displayedRepos = await sortRepoInfosByActivity(repos);
+
      sortByActivity = true;
+
    } finally {
+
      sorting = false;
+
    }
+
  }
</script>

<style>
@@ -35,6 +86,20 @@
    color: var(--color-text-tertiary);
    margin: 1rem;
  }
+
  .text-button {
+
    background: none;
+
    border: none;
+
    font: inherit;
+
    color: inherit;
+
    margin: 0;
+
    padding: 0;
+
  }
+
  .text-button:not(:disabled) {
+
    cursor: pointer;
+
  }
+
  .text-button:hover:not(:disabled) {
+
    text-decoration: underline;
+
  }

  @media (max-width: 1010.98px) {
    .repo-grid {
@@ -43,14 +108,14 @@
  }
</style>

-
{#await fetchRepoInfos(baseUrl, { show: "all", perPage: stats.repos.total }, utils.formatDid(did))}
+
{#await fetchRepos()}
  <div class="container">
    <Loading small center />
  </div>
{:then repos}
  {#if repos.length > 0}
    <div class="repo-grid">
-
      {#each repos as repoInfo}
+
      {#each displayedRepos as repoInfo (repoInfo.repo.rid)}
        <RepoCard {repoInfo} {baseUrl}>
          <svelte:fragment slot="delegate">
            <Badge
@@ -67,7 +132,13 @@
    </div>
    <div class="subtitle">
      {repos.length}
-
      {repos.length === 1 ? "repository" : "repositories"}
+
      {repos.length === 1 ? "repository" : "repositories"} ยท
+
      <button
+
        class="text-button"
+
        disabled={sorting}
+
        on:click={() => toggleSortByActivity(repos)}>
+
        {sortByActivity ? "Default order" : "Sort by activity"}
+
      </button>
    </div>
  {:else}
    <div class="container">