From 8751ba26ebf7b7761b9e237f2bf3453623dd1018 Mon Sep 17 00:00:00 2001
From: polwex <polwex@sortug.com>
Date: Mon, 6 Oct 2025 10:13:39 +0700
Subject: added frontend WS connection for demonstration purposes

---
 gui/src/components/post/Body.tsx | 174 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 174 insertions(+)
 create mode 100644 gui/src/components/post/Body.tsx

(limited to 'gui/src/components/post/Body.tsx')

diff --git a/gui/src/components/post/Body.tsx b/gui/src/components/post/Body.tsx
new file mode 100644
index 0000000..b4f1bb2
--- /dev/null
+++ b/gui/src/components/post/Body.tsx
@@ -0,0 +1,174 @@
+import type {
+  // TODO ref backend fetching!!
+  Reference,
+  Block,
+  Inline,
+  Media as MediaType,
+  ExternalContent,
+} from "@/types/trill";
+import Icon from "@/components/Icon";
+import type { PostProps } from "./Post";
+import Media from "./Media";
+import JSONContent, { YoutubeSnippet } from "./External";
+import { useLocation } from "wouter";
+import Quote from "./Quote";
+import PostData from "./Loader";
+import Card from "./Card.tsx";
+import type { Ship } from "@/types/urbit.ts";
+
+function Body(props: PostProps) {
+  const text = props.poast.contents.filter((c) => {
+    return (
+      "paragraph" in c ||
+      "blockquote" in c ||
+      "heading" in c ||
+      "codeblock" in c ||
+      "list" in c
+    );
+  });
+
+  const media: MediaType[] = props.poast.contents.filter(
+    (c): c is MediaType => "media" in c,
+  );
+
+  const refs = props.poast.contents.filter((c): c is Reference => "ref" in c);
+  const json = props.poast.contents.filter(
+    (c): c is ExternalContent => "json" in c,
+  );
+
+  return (
+    <div className="trill-post-body body">
+      <div className="body-text">
+        {text.map((b, i) => (
+          <TextBlock key={JSON.stringify(b) + i} block={b} />
+        ))}
+      </div>
+      {media.length > 0 && <Media media={media} />}
+      {refs.map((r, i) => (
+        <Ref r={r} nest={props.nest || 0} key={JSON.stringify(r) + i} />
+      ))}
+      <JSONContent content={json} />
+    </div>
+  );
+}
+export default Body;
+
+function TextBlock({ block }: { block: Block }) {
+  const key = JSON.stringify(block);
+  return "paragraph" in block ? (
+    <div className="trill-post-paragraph">
+      {block.paragraph.map((i, ind) => (
+        <Inlin key={key + ind} i={i} />
+      ))}
+    </div>
+  ) : "blockquote" in block ? (
+    <blockquote>
+      {block.blockquote.map((i, ind) => (
+        <Inlin key={key + ind} i={i} />
+      ))}
+    </blockquote>
+  ) : "heading" in block ? (
+    <Heading string={block.heading.text} num={block.heading.num} />
+  ) : "codeblock" in block ? (
+    <pre>
+      <code className={`language-${block.codeblock.lang}`}>
+        {block.codeblock.code}
+      </code>
+    </pre>
+  ) : "list" in block ? (
+    block.list.ordered ? (
+      <ol>
+        {block.list.text.map((i, ind) => (
+          <li key={JSON.stringify(i) + ind}>
+            <Inlin key={key + ind} i={i} />
+          </li>
+        ))}
+      </ol>
+    ) : (
+      <ul>
+        {block.list.text.map((i, ind) => (
+          <li key={JSON.stringify(i) + ind}>
+            <Inlin key={JSON.stringify(i) + ind} i={i} />
+          </li>
+        ))}
+      </ul>
+    )
+  ) : null;
+}
+function Inlin({ i }: { i: Inline }) {
+  const [_, navigate] = useLocation();
+  function gotoShip(e: React.MouseEvent, ship: Ship) {
+    e.stopPropagation();
+    navigate(`/feed/${ship}`);
+  }
+  return "text" in i ? (
+    <span>{i.text}</span>
+  ) : "italic" in i ? (
+    <i>{i.italic}</i>
+  ) : "bold" in i ? (
+    <strong>{i.bold}</strong>
+  ) : "strike" in i ? (
+    <span>{i.strike}</span>
+  ) : "underline" in i ? (
+    <span>{i.underline}</span>
+  ) : "sup" in i ? (
+    <sup>{i.sup}</sup>
+  ) : "sub" in i ? (
+    <sub>{i.sub}</sub>
+  ) : "ship" in i ? (
+    <span
+      className="mention"
+      role="link"
+      onMouseUp={(e) => gotoShip(e, i.ship)}
+    >
+      {i.ship}
+    </span>
+  ) : "codespan" in i ? (
+    <code>{i.codespan}</code>
+  ) : "link" in i ? (
+    <LinkParser {...i.link} />
+  ) : "break" in i ? (
+    <br />
+  ) : null;
+}
+
+function LinkParser({ href, show }: { href: string; show: string }) {
+  const YOUTUBE_REGEX_1 = /(youtube\.com\/watch\?v=)(\w+)/;
+  const YOUTUBE_REGEX_2 = /(youtu\.be\/)([a-zA-Z0-9-_]+)/;
+  const m1 = href.match(YOUTUBE_REGEX_1);
+  const m2 = href.match(YOUTUBE_REGEX_2);
+  const ytb = m1 && m1[2] ? m1[2] : m2 && m2[2] ? m2[2] : "";
+  return ytb ? (
+    <YoutubeSnippet href={href} id={ytb} />
+  ) : (
+    <a href={href}>{show}</a>
+  );
+}
+function Heading({ string, num }: { string: string; num: number }) {
+  return num === 1 ? (
+    <h1>{string}</h1>
+  ) : num === 2 ? (
+    <h2>{string}</h2>
+  ) : num === 3 ? (
+    <h3>{string}</h3>
+  ) : num === 4 ? (
+    <h4>{string}</h4>
+  ) : num === 5 ? (
+    <h5>{string}</h5>
+  ) : num === 6 ? (
+    <h6>{string}</h6>
+  ) : null;
+}
+
+function Ref({ r, nest }: { r: Reference; nest: number }) {
+  if (r.ref.type === "trill") {
+    const comp = PostData({
+      host: r.ref.ship,
+      id: r.ref.path.slice(1),
+      nest: nest + 1,
+      className: "quote-in-post",
+    })(Quote);
+    return <Card logo="crow">{comp}</Card>;
+  }
+  return <></>;
+}
-- 
cgit v1.2.3

