diff --git a/.eslintrc.js b/.eslintrc.js
index 00e8e5e8..da41e112 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -48,7 +48,6 @@ module.exports = {
"no-prototype-builtins": "off",
"react/display-name": "off",
"linebreak-style": ["error", "unix"],
- quotes: ["error", "double"],
semi: ["error", "always"],
"semi-spacing": ["error"],
"no-var": ["error"],
diff --git a/src/components/display/FileDisplay.tsx b/src/components/display/FileDisplay.tsx
index 4eaf3a83..47700957 100644
--- a/src/components/display/FileDisplay.tsx
+++ b/src/components/display/FileDisplay.tsx
@@ -25,13 +25,14 @@ import type { JSONType } from "@/types/json";
import AudioDisplay from "./AudioDisplay";
import CsvDisplay from "./CsvDisplay";
+import DocxDisplay from "./DocxDisplay";
+import HtmlDisplay from "./HtmlDisplay";
import ImageBlobDisplay from "./ImageBlobDisplay";
import JsonDisplay from "./JsonDisplay";
-import VideoDisplay from "./VideoDisplay";
-import XlsxDisplay from "./XlsxDisplay";
import MarkdownDisplay from "./MarkdownDisplay";
-import DocxDisplay from "./DocxDisplay";
import PdfDisplay from "./PdfDisplay";
+import VideoDisplay from "./VideoDisplay";
+import XlsxDisplay from "./XlsxDisplay";
import type { BlobDisplayProps } from "./types";
SyntaxHighlighter.registerLanguage("bash", bash);
@@ -85,6 +86,7 @@ export const VIEWABLE_FILE_EXTENSIONS = [
// Documents
"docx",
+ "html",
"pdf",
// Tabular data
@@ -260,6 +262,8 @@ const FileDisplay = ({ uri, fileName, loading }: FileDisplayProps) => {
return ;
} else if (fileExt === "json") {
return ;
+ } else if (fileExt === "html") {
+ return ;
} else {
return ;
}
diff --git a/src/components/display/HtmlDisplay.tsx b/src/components/display/HtmlDisplay.tsx
new file mode 100644
index 00000000..9fe4a280
--- /dev/null
+++ b/src/components/display/HtmlDisplay.tsx
@@ -0,0 +1,80 @@
+import { useEffect, useRef, useState } from "react";
+import { Spin } from "antd";
+
+import type { BlobDisplayProps } from "./types";
+import { LoadingOutlined } from "@ant-design/icons";
+
+const HtmlDisplay = ({ contents, loading }: BlobDisplayProps) => {
+ const iframeRef = useRef(null);
+
+ const [isConverting, setIsConverting] = useState(false);
+ const [iframeLoading, setIframeLoading] = useState(false);
+
+ // Effect used to define the iframe load listener used to turn off the iframe loading state after the iframe has
+ // finished loading the specified srcdoc (derived from contents.)
+ useEffect(() => {
+ const iframe = iframeRef.current;
+ if (!iframe) return;
+ const listener = () => {
+ setIframeLoading(false);
+ };
+ iframe.addEventListener("load", listener);
+ return () => iframe.removeEventListener("load", listener);
+ }, []);
+
+ useEffect(() => {
+ const iframe = iframeRef.current;
+
+ if (!iframe) return;
+ if (!contents) return;
+
+ setIsConverting(true);
+
+ contents
+ .text()
+ .then((html) => {
+ // If there isn't already a tag, instruct the embedded HTML to open links in the parent (i.e., here)
+ // instead of inside the iframe.
+ let modifiedHtml = html;
+ if (!modifiedHtml.includes("", '');
+ }
+
+ // When we update the srcdoc attribute, it'll cause the iframe to re-load the content.
+ // When it finishes loading, it'll trigger a load event listener on the iframe, defined above in another
+ // useEffect, which turns iframeLoading back off. This process lets us render a loading indicator.
+ iframe?.setAttribute("srcdoc", modifiedHtml);
+ setIframeLoading(true);
+ })
+ .finally(() => setIsConverting(false));
+ }, [contents]);
+
+ // Three different loading states:
+ // - loading bytes from server
+ // - converting bytes to text (essentially instant)
+ // - iframe loading/rendering HTML - triggered by updating the srcdoc attribute
+ const isLoading = loading || isConverting || iframeLoading;
+
+ return (
+ }>
+
+
+ );
+};
+
+export default HtmlDisplay;