Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add /raw route for getting images
Sebastian Martinez committed 3 years ago
commit 09ceeed136ce3d1599f9394914327e95b283a8bb
parent 3ea6a07dc4e378bd81ccf9d9ffc086be9f6b58af
10 files changed +40 -101
modified src/components/Comment.svelte
@@ -1,5 +1,4 @@
<script lang="ts">
-
  import type { MaybeBlob } from "@app/lib/project";
  import type { Comment, Thread } from "@app/lib/issue";

  import Authorship from "@app/components/Authorship.svelte";
@@ -8,7 +7,7 @@

  export let comment: Comment | Thread;
  export let caption = "commented";
-
  export let getImage: (path: string) => Promise<MaybeBlob>;
+
  export let rawPath: string;

  $: source = comment.author.id;
  $: title = comment.author.id;
@@ -57,7 +56,7 @@
      {#if comment.body.trim() === ""}
        <span class="txt-missing">No description.</span>
      {:else}
-
        <Markdown content={comment.body} {getImage} />
+
        <Markdown {rawPath} content={comment.body} />
      {/if}
    </div>
  </div>
modified src/components/Markdown.svelte
@@ -1,6 +1,4 @@
<script lang="ts">
-
  import type * as proj from "@app/lib/project";
-

  import dompurify from "dompurify";
  import matter from "@radicle/gray-matter";
  import { marked } from "marked";
@@ -8,7 +6,7 @@
  import { toDom } from "hast-util-to-dom";

  import { base } from "@app/lib/router";
-
  import { getImageMime, isUrl, twemoji, scrollIntoView } from "@app/lib/utils";
+
  import { isUrl, twemoji, scrollIntoView, canonicalize } from "@app/lib/utils";
  import { highlight } from "@app/lib/syntax";
  import {
    markdownExtensions as extensions,
@@ -17,8 +15,9 @@

  export let content: string;
  export let doc = matter(content);
-
  export let getImage: (path: string) => Promise<proj.MaybeBlob>;
  export let hash: string | null = null;
+
  export let path: string = "/";
+
  export let rawPath: string;

  const frontMatter = Object.entries(doc.data).filter(
    ([, val]) => typeof val === "string" || typeof val === "number",
@@ -43,22 +42,18 @@

    if (hash) scrollIntoView(hash);

-
    // Iterate over all images, and fetch their data from the API, then
-
    // replace the source with a Data-URL. We do this due to the absence
-
    // of a static file server.
+
    // Iterate over all images, and replace the source with a canonicalized URL
+
    // pointing at the projects /raw endpoint.
    for (const i of container.querySelectorAll("img")) {
-
      const path = i.getAttribute("src");
+
      const imagePath = i.getAttribute("src");

      // Make sure the source isn't a URL before trying to fetch it from the repo
-
      if (path && !isUrl(path) && !path.startsWith(`${base}twemoji`)) {
-
        getImage(path).then(blob => {
-
          if (blob?.content) {
-
            const mime = getImageMime(path);
-
            if (mime) {
-
              i.setAttribute("src", `data:${mime};base64,${blob.content}`);
-
            }
-
          }
-
        });
+
      if (
+
        imagePath &&
+
        !isUrl(imagePath) &&
+
        !imagePath.startsWith(`${base}twemoji`)
+
      ) {
+
        i.setAttribute("src", `${rawPath}/${canonicalize(imagePath, path)}`);
      }
    }

modified src/lib/project.ts
@@ -21,7 +21,7 @@ export enum ProjectContent {
}

export interface ProjectInfo {
-
  head: string | null;
+
  head: string;
  id: string;
  name: string;
  description: string;
@@ -125,7 +125,7 @@ export function parseRoute(

export class Project implements ProjectInfo {
  id: string;
-
  head: string | null;
+
  head: string;
  name: string;
  description: string;
  defaultBranch: string;
@@ -289,6 +289,12 @@ export class Project implements ProjectInfo {
    return result;
  }

+
  getRawPath(commit?: string): string {
+
    return `${this.seed.addr.scheme}://${this.seed.addr.host}:${
+
      this.seed.addr.port
+
    }/raw/${this.id}/${commit ?? this.head}`;
+
  }
+

  static async get(
    id: string,
    seedHost: string,
modified src/lib/utils.ts
@@ -119,28 +119,6 @@ export function capitalize(s: string): string {
  return s[0].toUpperCase() + s.substring(1);
}

-
// Get the mime type of an image, given a file path.
-
// Returns `null` if unknown.
-
export function getImageMime(path: string): string | null {
-
  const mimes: Record<string, string> = {
-
    apng: "image/apng",
-
    png: "image/png",
-
    svg: "image/svg+xml",
-
    gif: "image/gif",
-
    jpeg: "image/jpeg",
-
    jpg: "image/jpeg",
-
    webp: "image/webp",
-
  };
-
  const ext = path.split(".").pop();
-

-
  if (ext) {
-
    if (mimes[ext]) {
-
      return mimes[ext];
-
    }
-
  }
-
  return null;
-
}
-

// Takes a path, eg. "../images/image.png", and a base from where to start resolving, e.g. "static/images/index.html".
// Returns the resolved path.
export function canonicalize(
modified src/views/projects/Blob.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { MaybeBlob, Blob } from "@app/lib/project";
+
  import type { Blob } from "@app/lib/project";
  import type { MaybeHighlighted } from "@app/lib/syntax";
  import type { ProjectRoute } from "@app/lib/router/definitions";

@@ -14,7 +14,7 @@

  export let activeRoute: ProjectRoute;
  export let blob: Blob;
-
  export let getImage: (path: string) => Promise<MaybeBlob>;
+
  export let rawPath: string;
  export let line: string | undefined = undefined;

  const fileExtension = blob.path.split(".").pop() ?? "";
@@ -247,7 +247,7 @@
        <span class="txt-tiny">Binary content</span>
      </div>
    {:else if showMarkdown && blob.content}
-
      <Readme content={blob.content} {getImage} {activeRoute} />
+
      <Readme content={blob.content} {rawPath} {activeRoute} />
    {:else if content}
      <table class="code no-scrollbar">
        {@html toHtml(content)}
modified src/views/projects/Browser.svelte
@@ -11,7 +11,6 @@
  import type { ProjectRoute } from "@app/lib/router/definitions";

  import * as router from "@app/lib/router";
-
  import * as utils from "@app/lib/utils";
  import Button from "@app/components/Button.svelte";
  import Loading from "@app/components/Loading.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
@@ -59,15 +58,6 @@
    browserErrorStore.set(undefined);
  });

-
  // Get an image blob based on a relative path.
-
  const getImage = async (imagePath: string) => {
-
    const finalPath = utils.canonicalize(imagePath, path);
-
    return project.getBlob(commit, finalPath).catch(() => {
-
      console.warn("Not able to load image blob:", finalPath);
-
      return undefined;
-
    });
-
  };
-

  const onSelect = async (newPath: string) => {
    browserErrorStore.set(undefined);
    // Ensure we don't spend any time in a "loading" state. This means
@@ -242,7 +232,11 @@
            <Loading small center />
          {:then blob}
            {#if blob}
-
              <Blob {line} {blob} {getImage} {activeRoute} />
+
              <Blob
+
                {line}
+
                {blob}
+
                {activeRoute}
+
                rawPath={project.getRawPath(commit)} />
            {/if}
          {/await}
        {/if}
modified src/views/projects/Issue.svelte
@@ -1,23 +1,16 @@
<script lang="ts">
-
  import type { Blob, Project } from "@app/lib/project";
+
  import type { Project } from "@app/lib/project";
  import type { Issue } from "@app/lib/issue";

  import Authorship from "@app/components/Authorship.svelte";
  import Avatar from "@app/components/Comment/Avatar.svelte";
  import Chip from "@app/components/Chip.svelte";
  import Comment from "@app/components/Comment.svelte";
-
  import { formatNodeId, canonicalize, capitalize } from "@app/lib/utils";
+
  import { formatNodeId, capitalize } from "@app/lib/utils";
  import { formatObjectId } from "@app/lib/cobs";

  export let issue: Issue;
  export let project: Project;
-

-
  // 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>
@@ -140,7 +133,7 @@
  <main>
    <div class="comments">
      {#each issue.discussion as comment}
-
        <Comment {comment} {getImage} />
+
        <Comment {comment} rawPath={project.getRawPath()} />
      {/each}
    </div>
    <div class="metadata layout-desktop">
modified src/views/projects/Issue/New.svelte
@@ -1,5 +1,5 @@
<script lang="ts" strictEvents>
-
  import type { MaybeBlob, Project } from "@app/lib/project";
+
  import type { Project } from "@app/lib/project";
  import type { Session } from "@app/lib/session";

  import * as modal from "@app/lib/modal";
@@ -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, parseNodeId } from "@app/lib/utils";
+
  import { formatNodeId, parseNodeId } from "@app/lib/utils";
  import { createEventDispatcher } from "svelte";

  export let session: Session;
@@ -19,13 +19,6 @@

  const dispatch = createEventDispatcher<{ create: never }>();

-
  // Get an image blob based on a relative path.
-
  const getImage = async (imagePath: string): Promise<MaybeBlob> => {
-
    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);
-
  };
-

  let preview: boolean = false;

  function handleAddAssignee() {
@@ -181,14 +174,14 @@
        </div>
        <div class="comments">
          <Comment
+
            rawPath={project.getRawPath()}
            comment={{
              author: { id: session.publicKey },
              body: issueText,
              reactions: {},
              replyTo: null,
              timestamp: Date.now(),
-
            }}
-
            {getImage} />
+
            }} />
        </div>
      {:else}
        <Textarea bind:value={issueTitle} placeholder="Title" />
modified src/views/projects/Readme.svelte
@@ -1,13 +1,13 @@
<script lang="ts">
-
  import type * as proj from "@app/lib/project";
  import type { ProjectRoute } from "@app/lib/router/definitions";

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

  export let content: string;
-
  export let getImage: (path: string) => Promise<proj.MaybeBlob>;
+
  export let rawPath: string;
  export let activeRoute: ProjectRoute;

+
  $: path = activeRoute.params.path || "/";
  $: hash = activeRoute.params.hash || null;
</script>

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

<article>
-
  <Markdown {content} {getImage} {hash} />
+
  <Markdown {content} {hash} {rawPath} {path} />
</article>
modified tests/e2e/project.spec.ts
@@ -418,23 +418,4 @@ test.describe("browser error handling", () => {

    await expect(page.locator("text=Not able to load file")).toBeVisible();
  });
-
  test("error appears when a image with a relative path can't be loaded", async ({
-
    page,
-
  }) => {
-
    await page.route(
-
      `**/v1/projects/${rid}/blob/${aliceMainHead}/src/black-square.png`,
-
      route => route.fulfill({ status: 404 }),
-
    );
-

-
    await page.goto(projectFixtureUrl);
-
    const sourceTree = page.locator(".source-tree");
-
    await sourceTree.locator("text=markdown/").click();
-
    await sourceTree.locator("text=loading-image.md").click();
-

-
    // By having a relative path, this gives away that the image has not loaded
-
    // else it would have been converted into a data base64 string
-
    await expect(
-
      page.locator("img[src='../src/black-square.png']"),
-
    ).toBeVisible();
-
  });
});