summaryrefslogtreecommitdiff
path: root/gui/src/components/nostr
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-11-19 05:47:30 +0700
committerpolwex <polwex@sortug.com>2025-11-19 05:47:30 +0700
commit74d84cb2f22600b6246343e9ea606cf0db7517f0 (patch)
tree0d68285c8e74e6543645e17ab2751d543c1ff9a6 /gui/src/components/nostr
parente6e657be3a3b1dae426b46f3bc16f9a5cf4861c2 (diff)
Big GUI improvements on Nostr rendering and fetchingpolwex/iris
Diffstat (limited to 'gui/src/components/nostr')
-rw-r--r--gui/src/components/nostr/Feed.tsx53
-rw-r--r--gui/src/components/nostr/Thread.tsx174
-rw-r--r--gui/src/components/nostr/User.tsx62
3 files changed, 249 insertions, 40 deletions
diff --git a/gui/src/components/nostr/Feed.tsx b/gui/src/components/nostr/Feed.tsx
index d21307b..dec4ab5 100644
--- a/gui/src/components/nostr/Feed.tsx
+++ b/gui/src/components/nostr/Feed.tsx
@@ -5,6 +5,7 @@ import { useState } from "react";
import { eventsToFc } from "@/logic/nostrill";
import Icon from "@/components/Icon";
import toast from "react-hot-toast";
+import { Contact, RefreshCw } from "lucide-react";
export default function Nostr() {
const { nostrFeed, api, relays } = useLocalState((s) => ({
@@ -12,11 +13,8 @@ export default function Nostr() {
api: s.api,
relays: s.relays,
}));
- console.log({ relays });
const [isSyncing, setIsSyncing] = useState(false);
- console.log({ nostrFeed });
const feed = eventsToFc(nostrFeed);
- console.log({ feed });
const refetch = () => feed;
const handleResync = async () => {
@@ -34,6 +32,21 @@ export default function Nostr() {
}
};
+ async function fetchProfiles() {
+ if (!api) return;
+
+ setIsSyncing(true);
+ try {
+ await api.syncRelays();
+ toast.success("Nostr feed sync initiated");
+ } catch (error) {
+ toast.error("Failed to sync Nostr feed");
+ console.error("Sync error:", error);
+ } finally {
+ setIsSyncing(false);
+ }
+ }
+
if (Object.keys(relays).length === 0)
return (
<div className="nostr-empty-state">
@@ -97,18 +110,28 @@ export default function Nostr() {
{Object.keys(feed.feed).length} posts
</span>
</div>
- <button
- onClick={handleResync}
- disabled={isSyncing}
- className="resync-btn-small"
- title="Sync with Nostr relays"
- >
- {isSyncing ? (
- <img src={spinner} alt="Loading" className="btn-spinner-small" />
- ) : (
- <Icon name="settings" size={16} />
- )}
- </button>
+ <div className="flex gap-4">
+ <button
+ className="btn-small"
+ onClick={fetchProfiles}
+ title="Fetch user profiles"
+ >
+ <Contact />
+ </button>
+
+ <button
+ onClick={handleResync}
+ disabled={isSyncing}
+ className="btn-small"
+ title="Sync with Nostr relays"
+ >
+ {isSyncing ? (
+ <img src={spinner} alt="Loading" className="btn-spinner-small" />
+ ) : (
+ <RefreshCw />
+ )}
+ </button>
+ </div>
</div>
<PostList data={feed} refetch={refetch} />
</div>
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>
+ );
+}
diff --git a/gui/src/components/nostr/User.tsx b/gui/src/components/nostr/User.tsx
index a9e9e2f..be7b15c 100644
--- a/gui/src/components/nostr/User.tsx
+++ b/gui/src/components/nostr/User.tsx
@@ -1,34 +1,52 @@
import useLocalState from "@/state/state";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import Icon from "@/components/Icon";
import toast from "react-hot-toast";
-import type { UserType } from "@/types/nostrill";
import type { FC } from "@/types/trill";
import Composer from "../composer/Composer";
import PostList from "@/components/feed/PostList";
+import { addEventToFc, eventsToFc } from "@/logic/nostrill";
export default function NostrUser({
- user,
userString,
+ pubkey,
feed,
isFollowLoading,
setIsFollowLoading,
isAccessLoading,
setIsAccessLoading,
}: {
- user: UserType;
userString: string;
+ pubkey: string;
feed: FC | undefined;
isFollowLoading: boolean;
setIsFollowLoading: (b: boolean) => void;
isAccessLoading: boolean;
setIsAccessLoading: (b: boolean) => void;
}) {
- const { api } = useLocalState((s) => ({
+ const { api, addNotification, lastFact } = useLocalState((s) => ({
api: s.api,
+ addNotification: s.addNotification,
+ lastFact: s.lastFact,
}));
const [fc, setFC] = useState<FC>();
+ useEffect(() => {
+ if (!lastFact) return;
+ if (!("nostr" in lastFact)) return;
+ if ("user" in lastFact.nostr) {
+ const feed = eventsToFc(lastFact.nostr.user);
+ setFC(feed);
+ } else if ("event" in lastFact.nostr) {
+ const ev = lastFact.nostr.event;
+ if (ev.kind === 1 && ev.pubkey === pubkey) {
+ const f = feed || fc;
+ if (!f) return;
+ const nf = addEventToFc(ev, f);
+ setFC(nf);
+ }
+ }
+ }, [lastFact]);
// Show empty state with resync option when no feed data
async function refetch() {
@@ -39,6 +57,7 @@ export default function NostrUser({
setIsFollowLoading(true);
try {
+ const user = { nostr: pubkey };
if (feed) {
await api.unfollow(user);
} else {
@@ -55,26 +74,19 @@ export default function NostrUser({
if (!api) return;
setIsAccessLoading(true);
- // try {
- // const res = await api.peekFeed(user.urbit);
- // toast.success(`Access request sent to ${user.urbit}`);
- // addNotification({
- // type: "access_request",
- // from: userString,
- // message: `Access request sent to ${userString}`,
- // });
- // if ("error" in res) toast.error(res.error);
- // else {
- // console.log("peeked", res.ok.feed);
- // setFC(res.ok.feed);
- // if (res.ok.profile) addProfile(userString, res.ok.profile);
- // }
- // } catch (error) {
- // toast.error(`Failed to request access from ${user.urbit}`);
- // console.error("Access request error:", error);
- // } finally {
- // setIsAccessLoading(false);
- // }
+ try {
+ await api.nostrFeed(pubkey);
+ addNotification({
+ type: "fetching_nostr",
+ from: userString,
+ message: `Fetching nostr feed from ${userString}`,
+ });
+ } catch (error) {
+ toast.error(`Failed to request access from ${user.urbit}`);
+ console.error("Access request error:", error);
+ } finally {
+ setIsAccessLoading(false);
+ }
}
return (
<>