Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Fix embeds in `Markdown` component
Merged did:key:z6MkkfM3...sVz5 opened 1 year ago

Changes the display of embeds from being markdown images to markdown links. Mimics the way embeds work on the desktop app and allows other embeds to work too like pdfs, videos, etc.

  • httpd: Increase MAX_BLOB_SIZE to 10 MB

  • httpd: Infer mime type of file blob in file_by_oid handler

    Instead of relying on the mime_type query string the endpoint should try to infer the file type.

check check-visual check-unit-test check-http-client-unit-test check-radicle-httpd check-e2e check-build check-http

👉 Preview 👉 Workflow runs 👉 Branch on GitHub

5 files changed +98 -32 98ad210b 0d7a96e3
modified radicle-httpd/Cargo.lock
@@ -376,6 +376,17 @@ dependencies = [
]

[[package]]
+
name = "cfb"
+
version = "0.7.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
+
dependencies = [
+
 "byteorder",
+
 "fnv",
+
 "uuid",
+
]
+

+
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1006,6 +1017,15 @@ dependencies = [
]

[[package]]
+
name = "infer"
+
version = "0.16.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847"
+
dependencies = [
+
 "cfb",
+
]
+

+
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1675,7 +1695,7 @@ dependencies = [

[[package]]
name = "radicle-httpd"
-
version = "0.18.0"
+
version = "0.18.1"
dependencies = [
 "anyhow",
 "axum",
@@ -1683,6 +1703,7 @@ dependencies = [
 "chrono",
 "flate2",
 "hyper",
+
 "infer",
 "lexopt",
 "lru",
 "nonempty",
@@ -2565,6 +2586,12 @@ dependencies = [
]

[[package]]
+
name = "uuid"
+
version = "1.12.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
+

+
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
modified radicle-httpd/Cargo.toml
@@ -3,7 +3,7 @@ name = "radicle-httpd"
description = "Radicle HTTP daemon"
homepage = "https://radicle.xyz"
license = "MIT OR Apache-2.0"
-
version = "0.18.0"
+
version = "0.18.1"
authors = ["cloudhead <cloudhead@radicle.xyz>"]
edition = "2021"
default-run = "radicle-httpd"
@@ -27,6 +27,7 @@ base64 = { version = "0.22.1" }
chrono = { version = "0.4.38", default-features = false, features = ["clock"] }
flate2 = { version = "1" }
hyper = { version = "1.4", default-features = false }
+
infer = { version = "0.16.0" }
lexopt = { version = "0.3.0" }
lru = { version = "0.12.4" }
nonempty = { version = "0.9.0", features = ["serialize"] }
modified radicle-httpd/src/raw.rs
@@ -17,7 +17,7 @@ use crate::api::query::RawQuery;
use crate::axum_extra::Path;
use crate::error::RawError as Error;

-
const MAX_BLOB_SIZE: usize = 4_194_304;
+
const MAX_BLOB_SIZE: usize = 10_485_760;

static MIMES: &[(&str, &str)] = &[
    ("3gp", "video/3gpp"),
@@ -161,7 +161,7 @@ fn blob_response(
async fn file_by_oid_handler(
    Path((rid, oid)): Path<(RepoId, Oid)>,
    State(profile): State<Arc<Profile>>,
-
    Query(qs): Query<RawQuery>,
+
    Query(_qs): Query<RawQuery>,
) -> impl IntoResponse {
    let storage = &profile.storage;
    let repo = storage.repository(rid)?;
@@ -172,6 +172,8 @@ async fn file_by_oid_handler(
    }

    let blob = repo.blob(oid)?;
+
    let content = blob.content();
+
    let mime = infer::get(content).map(|i| i.mime_type().to_string());
    let mut response_headers = HeaderMap::new();

    if blob.size() > MAX_BLOB_SIZE {
@@ -180,10 +182,10 @@ async fn file_by_oid_handler(

    response_headers.insert(
        header::CONTENT_TYPE,
-
        HeaderValue::from_str(&qs.mime.unwrap_or("application/octet-stream".to_string()))?,
+
        HeaderValue::from_str(&mime.unwrap_or("application/octet-stream".to_string()))?,
    );

-
    Ok::<_, Error>((StatusCode::OK, response_headers, blob.content().to_vec()))
+
    Ok::<_, Error>((StatusCode::OK, response_headers, content.to_vec()))
}

#[cfg(test)]
modified src/components/Markdown.svelte
@@ -1,6 +1,4 @@
<script lang="ts">
-
  import type { Embed } from "@http-client";
-

  import dompurify from "dompurify";
  import matter from "@radicle/gray-matter";
  import { afterUpdate } from "svelte";
@@ -24,9 +22,6 @@
  export let content: string;
  export let path: string = "/";
  export let rawPath: string;
-
  // If present, means we are in a preview context,
-
  // use this for image previews instead of /raw URLs.
-
  export let embeds: Map<string, Embed> | undefined = undefined;
  // If true, add <br> on a single line break
  export let breaks: boolean = false;

@@ -114,6 +109,67 @@
      } catch (e) {
        console.warn("Not able to parse url", e);
      }
+

+
      const anchorHref = e.getAttribute("href");
+

+
      // If the anchor is an oid embed
+
      if (anchorHref && isCommit(anchorHref)) {
+
        const url = new URL(rawPath);
+
        // deprecated with httpd 0.18.1
+
        // For older httpd versions we still pass the mime type.
+
        // On newer radicle-httpd instances we try to infer the file type on httpd.
+
        const fileExtension = e.innerText.split(".").pop();
+
        if (fileExtension && fileExtension in mimes) {
+
          url.search = `?mime=${mimes[fileExtension]}`;
+
        }
+
        url.pathname = canonicalize(`blobs/${anchorHref}`, url.pathname);
+
        e.setAttribute("href", url.toString());
+

+
        // To determine the filetype of the embed we query the content-type of the resource URL.
+
        const req = await fetch(url, { method: "HEAD" });
+
        const mimeType = req.headers.get("Content-Type");
+

+
        // Embed an img element below the link
+
        if (mimeType?.startsWith("image")) {
+
          const element = document.createElement("img");
+
          element.setAttribute("src", url.toString());
+
          element.style.display = "block";
+
          e.style.display = "block";
+
          e.insertAdjacentElement("afterend", element);
+
          // Embed an iframe to display pdf correctly element below the link
+
        } else if (mimeType?.startsWith("application/pdf")) {
+
          const element = document.createElement("embed");
+
          element.setAttribute("src", url.toString());
+
          element.type = mimeType;
+
          element.style.overflow = "scroll";
+
          element.style.height = "40rem";
+
          element.style.overscrollBehavior = "contain";
+
          e.style.display = "block";
+
          e.insertAdjacentElement("afterend", element);
+
        } else if (mimeType?.startsWith("video")) {
+
          const element = document.createElement("video");
+
          const node = document.createElement("source");
+
          node.src = url.toString();
+
          element.controls = true;
+
          node.type = mimeType;
+
          element.style.width = "100%";
+
          e.style.display = "block";
+
          element.appendChild(node);
+
          e.insertAdjacentElement("afterend", element);
+
        } else if (mimeType?.startsWith("audio")) {
+
          const element = document.createElement("audio");
+
          element.style.display = "block";
+
          element.src = url.toString();
+
          element.controls = true;
+
          e.style.display = "block";
+
          e.insertAdjacentElement("afterend", element);
+
        } else {
+
          console.warn(`Not able to provide a preview for this file.`);
+
        }
+

+
        continue;
+
      }
+

      // Don't underline <a> tags that contain images.
      // Make an exception for emojis.
      if (
@@ -144,27 +200,6 @@
      const imagePath = i.getAttribute("src");
      const imageClass = i.getAttribute("class");

-
      // If the image is an oid embed
-
      if (imagePath && isCommit(imagePath)) {
-
        const embed = embeds?.get(imagePath);
-
        // If the embed content is the base64 encoded image, use it directly.
-
        if (embed && embed.content.startsWith("data:")) {
-
          i.setAttribute("src", embed.content);
-
          continue;
-
        }
-

-
        const fileExtension = i.alt.split(".").pop();
-
        const url = new URL(rawPath);
-
        // If a user changes the alt text of an image,
-
        // the browser is still able to infer the mime type.
-
        if (fileExtension && fileExtension in mimes) {
-
          url.search = `?mime=${mimes[fileExtension]}`;
-
        }
-
        url.pathname = canonicalize(`blobs/${imagePath}`, url.pathname);
-
        i.setAttribute("src", url.toString());
-
        continue;
-
      }
-

      // Make sure the source isn't a URL before trying to fetch it from the repo
      const emoji = imageClass && imageClass === "txt-emoji";
      if (imagePath && !isUrl(imagePath) && !emoji) {
modified src/lib/file.ts
@@ -27,6 +27,7 @@ export const mimes: Record<string, string> = {
  mp3: "audio/mpeg",
  mp4: "video/mp4",
  mpeg: "video/mpeg",
+
  mov: "video/mp4",
  odp: "application/vnd.oasis.opendocument.presentation",
  ods: "application/vnd.oasis.opendocument.spreadsheet",
  odt: "application/vnd.oasis.opendocument.text",