Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Infinite patch listing scroll
Merged did:key:z6MkkfM3...sVz5 opened 1 year ago
12 files changed +149 -53 6b8c73c6 517d867f
added src-tauri/bindings/PaginatedQuery.ts
@@ -0,0 +1,3 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
export type PaginatedQuery<T> = { cursor: number; more: boolean; content: T };
modified src-tauri/src/commands/cob.rs
@@ -34,17 +34,25 @@ mod query {
        Draft,
        Archived,
        Merged,
-
        All,
    }

-
    impl PatchStatus {
-
        pub fn matches(&self, patch: &patch::State) -> bool {
-
            match self {
-
                Self::Open => matches!(patch, patch::State::Open { .. }),
-
                Self::Draft => matches!(patch, patch::State::Draft),
-
                Self::Archived => matches!(patch, patch::State::Archived),
-
                Self::Merged => matches!(patch, patch::State::Merged { .. }),
-
                Self::All => true,
+
    impl From<patch::Status> for PatchStatus {
+
        fn from(value: patch::Status) -> Self {
+
            match value {
+
                patch::Status::Archived => Self::Archived,
+
                patch::Status::Draft => Self::Draft,
+
                patch::Status::Merged => Self::Merged,
+
                patch::Status::Open => Self::Open,
+
            }
+
        }
+
    }
+
    impl From<PatchStatus> for patch::Status {
+
        fn from(value: PatchStatus) -> Self {
+
            match value {
+
                PatchStatus::Archived => Self::Archived,
+
                PatchStatus::Draft => Self::Draft,
+
                PatchStatus::Merged => Self::Merged,
+
                PatchStatus::Open => Self::Open,
            }
        }
    }
modified src-tauri/src/commands/cob/patch.rs
@@ -2,36 +2,59 @@ use radicle::git;
use radicle::identity::RepoId;
use radicle::patch::cache::Patches;
use radicle::storage::ReadStorage;
+
use serde::{Deserialize, Serialize};
+
use ts_rs::TS;

-
use crate::cob::query;
use crate::error::Error;
use crate::types::cobs;
use crate::AppState;

+
use crate::cob::query;
+

+
#[derive(Serialize, Deserialize, TS)]
+
#[ts(export)]
+
pub struct PaginatedQuery<T> {
+
    pub cursor: usize,
+
    pub more: bool,
+
    pub content: T,
+
}
+

#[tauri::command]
-
pub fn list_patches(
-
    ctx: tauri::State<AppState>,
+
pub async fn list_patches(
+
    ctx: tauri::State<'_, AppState>,
    rid: RepoId,
-
    status: query::PatchStatus,
-
) -> Result<Vec<cobs::Patch>, Error> {
+
    status: Option<query::PatchStatus>,
+
    skip: Option<usize>,
+
    take: Option<usize>,
+
) -> Result<PaginatedQuery<Vec<cobs::Patch>>, Error> {
+
    let cursor = skip.unwrap_or(0);
+
    let take = take.unwrap_or(20);
    let repo = ctx.profile.storage.repository(rid)?;
-
    let patches = ctx.profile.patches(&repo)?;
-
    let mut patches: Vec<_> = patches
-
        .list()?
-
        .filter_map(|r| {
-
            let (id, patch) = r.ok()?;
-
            (status.matches(patch.state())).then_some((id, patch))
-
        })
-
        .collect::<Vec<_>>();
-

-
    patches.sort_by(|(_, a), (_, b)| b.timestamp().cmp(&a.timestamp()));
    let aliases = &ctx.profile.aliases();
-
    let patches = patches
+
    let cache = ctx.profile.patches(&repo)?;
+
    let patches = match status {
+
        None => cache.list()?.collect::<Vec<_>>(),
+
        Some(s) => cache.list_by_status(&s.into())?.collect::<Vec<_>>(),
+
    };
+
    let more = cursor + take < patches.len();
+

+
    let mut patches = patches
        .into_iter()
-
        .map(|(id, patch)| cobs::Patch::new(id, patch, aliases))
+
        .filter_map(|p| {
+
            p.map(|(id, patch)| cobs::Patch::new(id, patch, aliases))
+
                .ok()
+
        })
+
        .skip(cursor)
+
        .take(take)
        .collect::<Vec<_>>();

-
    Ok::<_, Error>(patches)
+
    patches.sort_by_key(|b| std::cmp::Reverse(b.timestamp()));
+

+
    Ok::<_, Error>(PaginatedQuery {
+
        cursor,
+
        more,
+
        content: patches,
+
    })
}

#[tauri::command]
modified src-tauri/src/types/cobs.rs
@@ -119,6 +119,10 @@ impl Patch {
            revision_count: patch.revisions().count(),
        }
    }
+

+
    pub fn timestamp(&self) -> u64 {
+
        self.timestamp.as_millis()
+
    }
}

#[derive(Serialize, TS)]
modified src/components/PatchTeaser.svelte
@@ -9,6 +9,7 @@
    patchStatusColor,
  } from "@app/lib/utils";
  import { invoke } from "@tauri-apps/api/core";
+
  import { onMount } from "svelte";
  import { push } from "@app/lib/router";

  import DiffStatBadge from "./DiffStatBadge.svelte";
@@ -16,6 +17,16 @@
  import InlineTitle from "./InlineTitle.svelte";
  import NodeId from "./NodeId.svelte";

+
  let stats: Stats | undefined = undefined;
+

+
  onMount(async () => {
+
    stats = await invoke<Stats>("diff_stats", {
+
      rid,
+
      base: patch.base,
+
      head: patch.head,
+
    });
+
  });
+

  export let patch: Patch;
  export let rid: string;
</script>
@@ -81,9 +92,9 @@
    </div>
  </div>
  <div class="global-flex">
-
    {#await invoke<Stats>( "diff_stats", { rid, base: patch.base, head: patch.head }, ) then stats}
+
    {#if stats}
      <DiffStatBadge {stats} />
-
    {/await}
+
    {/if}
    {#each patch.labels as label}
      <div class="global-counter txt-small">{label}</div>
    {/each}
modified src/lib/router.ts
@@ -82,6 +82,11 @@ async function navigate(
  activeRouteStore.set(loadedRoute);
  activeUnloadedRouteStore.set(newRoute);
  isLoading.set(false);
+
  Array.from(
+
    document.getElementsByClassName("global-reset-scroll-after-navigate"),
+
  ).forEach(el => {
+
    el.scrollTo(0, 0);
+
  });
}

export async function push(newRoute: Route): Promise<void> {
modified src/views/repo/Issue.svelte
@@ -52,7 +52,7 @@
  }
</style>

-
<Layout {repo} selfDid={`did:key:${config.publicKey}`}>
+
<Layout>
  <svelte:fragment slot="breadcrumbs">
    <Link route={{ resource: "home" }}>
      <NodeId
modified src/views/repo/Issues.svelte
@@ -100,9 +100,7 @@
    </div>

    <div class="global-flex txt-small" style:margin="0.5rem 0">
-
      <Link
-
        variant="tab"
-
        route={{ resource: "repo.patches", rid: repo.rid, status: "all" }}>
+
      <Link variant="tab" route={{ resource: "repo.patches", rid: repo.rid }}>
        <div class="global-flex"><Icon name="patch" />Patches</div>
        <div class="global-counter">
          {project.meta.patches.draft +
modified src/views/repo/Layout.svelte
@@ -1,9 +1,30 @@
<script lang="ts">
+
  import { onMount } from "svelte";
+

  import Header from "@app/components/Header.svelte";
  import Icon from "@app/components/Icon.svelte";
  import NakedButton from "@app/components/NakedButton.svelte";

+
  export let loadMore: (() => Promise<void>) | undefined = undefined;
+

  let hidden = false;
+
  let listElement: HTMLElement;
+
  let loading = false;
+

+
  onMount(() => {
+
    if (listElement && loadMore) {
+
      listElement.addEventListener("scroll", async () => {
+
        if (
+
          listElement.scrollTop + listElement.clientHeight >=
+
            listElement.scrollHeight - 600 &&
+
          loading === false
+
        ) {
+
          loading = true;
+
          void loadMore().finally(() => (loading = false));
+
        }
+
      });
+
    }
+
  });
</script>

<style>
@@ -64,7 +85,9 @@
    <slot name="sidebar" />
  </div>

-
  <div class="content">
+
  <div
+
    class="content global-reset-scroll-after-navigate"
+
    bind:this={listElement}>
    <slot />
  </div>
</div>
modified src/views/repo/Patch.svelte
@@ -54,7 +54,7 @@
  }
</style>

-
<Layout {repo} selfDid={`did:key:${config.publicKey}`}>
+
<Layout>
  <svelte:fragment slot="breadcrumbs">
    <Link route={{ resource: "home" }}>
      <NodeId
modified src/views/repo/Patches.svelte
@@ -1,11 +1,13 @@
<script lang="ts">
  import type { Config } from "@bindings/Config";
+
  import type { PaginatedQuery } from "@bindings/PaginatedQuery";
  import type { Patch } from "@bindings/Patch";
  import type { PatchStatus } from "./router";
  import type { RepoInfo } from "@bindings/RepoInfo";

-
  import Layout from "./Layout.svelte";
+
  import { invoke } from "@tauri-apps/api/core";

+
  import Layout from "./Layout.svelte";
  import Border from "@app/components/Border.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
  import Icon from "@app/components/Icon.svelte";
@@ -15,9 +17,28 @@
  import RepoHeader from "@app/components/RepoHeader.svelte";

  export let repo: RepoInfo;
-
  export let patches: Patch[];
+
  export let patches: PaginatedQuery<Patch[]>;
  export let config: Config;
-
  export let status: PatchStatus;
+
  export let status: PatchStatus | undefined = undefined;
+

+
  $: items = patches.content;
+
  $: more = patches.more;
+
  $: cursor = patches.cursor;
+

+
  async function loadMore() {
+
    if (more) {
+
      const p = await invoke<PaginatedQuery<Patch[]>>("list_patches", {
+
        rid: repo.rid,
+
        skip: cursor + 20,
+
        status,
+
        take: 20,
+
      });
+

+
      cursor = p.cursor;
+
      more = p.more;
+
      items = [...items, ...p.content];
+
    }
+
  }

  $: project = repo.payloads["xyz.radicle.project"]!;
</script>
@@ -31,7 +52,7 @@
  }
</style>

-
<Layout>
+
<Layout {loadMore}>
  <svelte:fragment slot="breadcrumbs">
    <Link route={{ resource: "home" }}>
      <NodeId
@@ -77,8 +98,8 @@
    </div>
    <div class="global-flex txt-small" style:margin="0.5rem 0">
      <Link
-
        variant={status === "all" ? "active" : "tab"}
-
        route={{ resource: "repo.patches", rid: repo.rid, status: "all" }}>
+
        variant={status === undefined ? "active" : "tab"}
+
        route={{ resource: "repo.patches", rid: repo.rid }}>
        <div class="global-flex"><Icon name="patch" />Patches</div>
        <div class="global-counter">
          {project.meta.patches.draft +
@@ -133,13 +154,13 @@
  </svelte:fragment>

  <div class="list">
-
    {#each patches as patch}
+
    {#each items as patch}
      <PatchTeaser rid={repo.rid} {patch} />
    {/each}

-
    {#if patches.length === 0}
+
    {#if patches.content.length === 0}
      <div class="txt-missing txt-small">
-
        {#if status === "all"}
+
        {#if status === undefined}
          No patches.
        {:else}
          No {status} patches.
modified src/views/repo/router.ts
@@ -1,4 +1,5 @@
import type { Config } from "@bindings/Config";
+
import type { PaginatedQuery } from "@bindings/PaginatedQuery";
import type { Issue } from "@bindings/Issue";
import type { Patch } from "@bindings/Patch";
import type { RepoInfo } from "@bindings/RepoInfo";
@@ -41,7 +42,7 @@ export interface LoadedRepoIssuesRoute {
  };
}

-
export type PatchStatus = "all" | Patch["state"]["status"];
+
export type PatchStatus = Patch["state"]["status"];

export interface RepoPatchRoute {
  resource: "repo.patch";
@@ -63,7 +64,7 @@ export interface LoadedRepoPatchRoute {
export interface RepoPatchesRoute {
  resource: "repo.patches";
  rid: string;
-
  status: PatchStatus;
+
  status?: PatchStatus;
}

export interface LoadedRepoPatchesRoute {
@@ -71,8 +72,8 @@ export interface LoadedRepoPatchesRoute {
  params: {
    repo: RepoInfo;
    config: Config;
-
    patches: Patch[];
-
    status: PatchStatus;
+
    patches: PaginatedQuery<Patch[]>;
+
    status?: PatchStatus;
  };
}

@@ -94,9 +95,8 @@ export async function loadPatch(
    rid: route.rid,
  });
  const config: Config = await invoke("config");
-
  const patches: Patch[] = await invoke("list_patches", {
+
  const patches: PaginatedQuery<Patch[]> = await invoke("list_patches", {
    rid: route.rid,
-
    status: "all",
  });
  const patch: Patch = await invoke("patch_by_id", {
    rid: route.rid,
@@ -109,7 +109,7 @@ export async function loadPatch(

  return {
    resource: "repo.patch",
-
    params: { repo, config, patch, patches, revisions },
+
    params: { repo, config, patch, patches: patches.content, revisions },
  };
}

@@ -120,7 +120,7 @@ export async function loadPatches(
    rid: route.rid,
  });
  const config: Config = await invoke("config");
-
  const patches: Patch[] = await invoke("list_patches", {
+
  const patches: PaginatedQuery<Patch[]> = await invoke("list_patches", {
    rid: route.rid,
    status: route.status,
  });
@@ -243,7 +243,7 @@ export function repoUrlToRoute(
        ) {
          return { resource: "repo.patches", rid, status };
        } else {
-
          return { resource: "repo.patches", rid, status: "all" };
+
          return { resource: "repo.patches", rid };
        }
      }
    } else {