summaryrefslogtreecommitdiff
path: root/gui/src/components/nostr/Thread.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src/components/nostr/Thread.tsx')
-rw-r--r--gui/src/components/nostr/Thread.tsx174
1 files changed, 174 insertions, 0 deletions
diff --git a/gui/src/components/nostr/Thread.tsx b/gui/src/components/nostr/Thread.tsx
new file mode 100644
index 0000000..c46b547
--- /dev/null
+++ b/gui/src/components/nostr/Thread.tsx
@@ -0,0 +1,174 @@
+import useLocalState from "@/state/state";
+import Icon from "@/components/Icon";
+import spinner from "@/assets/triangles.svg";
+import type { FC, FullFeed, FullNode } from "@/types/trill";
+import Composer from "@/components/composer/Composer";
+import type { UserProfile } from "@/types/nostrill";
+import { useEffect, useState } from "react";
+import toast from "react-hot-toast";
+import { eventsToFF, eventToFn } from "@/logic/trill/helpers";
+import { toFlat } from "../post/RP";
+import type { NostrEvent } from "@/types/nostr";
+import { createCache } from "@/logic/cache";
+import Post from "../post/Post";
+import Modal from "../modals/Modal";
+
+type Props = {
+ host: string;
+ id: string;
+ feed?: FC;
+ profile?: UserProfile;
+};
+const cache = createCache({ dbName: "nostrill", storeName: "nosted" });
+
+export default function Thread(props: Props) {
+ const { api, composerData, setComposerData, setModal, lastFact } =
+ useLocalState((s) => ({
+ api: s.api,
+ lastFact: s.lastFact,
+ composerData: s.composerData,
+ setComposerData: s.setComposerData,
+ setModal: s.setModal,
+ }));
+ const { id, feed, profile } = props;
+ const poast = feed?.feed[id];
+ const host = poast?.author || "";
+ const [error, setError] = useState("");
+ // const [data, setData] = useState<{fc: FC, head: Poast}>(() => getCachedData(id));
+ const [data, setData] = useState<FullFeed>();
+
+ useEffect(() => {
+ console.log({ composerData });
+ if (composerData)
+ setModal(
+ <Modal
+ close={() => {
+ setComposerData(null);
+ }}
+ >
+ <Composer />
+ </Modal>,
+ );
+ }, [composerData]);
+ // useTimeout(() => {
+ // if (!data) setError("Request timed out");
+ // }, 10_000);
+
+ useEffect(() => {
+ if (!lastFact) return;
+ if (!("nostr" in lastFact)) return;
+ if (!("thread" in lastFact.nostr)) return;
+ toast.success("thread fetched succesfully, rendering");
+ cache.set("evs", lastFact.nostr.thread);
+ const nodes = lastFact.nostr.thread.map(eventToFn);
+ const ff = eventsToFF(nodes);
+ setData(ff);
+ }, [lastFact]);
+
+ useEffect(() => {
+ if (!api) return;
+ const init = async () => {
+ const cached: NostrEvent[] | null = await cache.get("evs");
+ if (cached) {
+ const nodes = cached.map(eventToFn);
+ const ff = eventsToFF(nodes);
+ setData(ff);
+ }
+ };
+ init();
+ }, [id]);
+
+ async function tryAgain() {
+ if (!api) return;
+ setError("");
+ api.nostrThread(id);
+ }
+
+ return (
+ <>
+ <div className="thread-header">
+ <div className="thread-nav">
+ <button
+ className="back-btn"
+ onClick={() => window.history.back()}
+ title="Go back"
+ >
+ <Icon name="reply" size={16} />
+ <span>Back to Feed</span>
+ </button>
+ </div>
+ <h2>Thread</h2>
+ <div className="thread-info">
+ <span className="thread-host">~{host}</span>
+ <span className="thread-separator">•</span>
+ <span className="thread-id">#{id}</span>
+ </div>
+ </div>
+ <div id="feed-proper">
+ {data ? (
+ <>
+ <Head node={data[id]} profile={profile} />
+ </>
+ ) : error ? (
+ <div className="text-center m-10 text-2xl">
+ <h2>Error Loading Thread</h2>
+ <p className="error">{error}</p>
+ <button className="cycle-btn mx-auto my-8" onClick={tryAgain}>
+ Try Again
+ </button>
+ </div>
+ ) : (
+ <>
+ <h2 className="text-center my-8">Loading Thread...</h2>
+ <div className="loading-container">
+ <img className="x-center" src={spinner} alt="Loading" />
+ </div>
+ </>
+ )}
+ </div>
+ </>
+ );
+}
+function Head({ node, profile }: { node: FullNode; profile?: UserProfile }) {
+ return (
+ <>
+ <Post
+ poast={toFlat(node)}
+ user={{ nostr: node.author }}
+ profile={profile}
+ />
+ <div id="thread-children">
+ <Minithread ff={node.children} />
+ </div>
+ </>
+ );
+}
+
+function Minithread({ ff }: { ff: FullFeed }) {
+ const profiles = useLocalState((s) => s.profiles);
+ const nodes = Object.values(ff);
+ return (
+ <div id="tail">
+ {nodes.map((c) => {
+ const profile = profiles.get(c.author);
+ return (
+ <div key={c.hash} className="minithread">
+ <Post
+ user={{ nostr: c.author }}
+ poast={toFlat(c)}
+ profile={profile}
+ />
+ <Grandchildren node={c} />
+ </div>
+ );
+ })}
+ </div>
+ );
+}
+function Grandchildren({ node }: { node: FullNode }) {
+ return (
+ <div className="tail">
+ <Minithread ff={node.children} />
+ </div>
+ );
+}