From 74d84cb2f22600b6246343e9ea606cf0db7517f0 Mon Sep 17 00:00:00 2001 From: polwex Date: Wed, 19 Nov 2025 05:47:30 +0700 Subject: Big GUI improvements on Nostr rendering and fetching --- gui/src/components/nostr/Feed.tsx | 53 +++++++---- gui/src/components/nostr/Thread.tsx | 174 ++++++++++++++++++++++++++++++++++++ gui/src/components/nostr/User.tsx | 62 +++++++------ 3 files changed, 249 insertions(+), 40 deletions(-) create mode 100644 gui/src/components/nostr/Thread.tsx (limited to 'gui/src/components/nostr') 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 (
@@ -97,18 +110,28 @@ export default function Nostr() { {Object.keys(feed.feed).length} posts
- +
+ + + +
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(); + + useEffect(() => { + console.log({ composerData }); + if (composerData) + setModal( + { + setComposerData(null); + }} + > + + , + ); + }, [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 ( + <> +
+
+ +
+

Thread

+
+ ~{host} + + #{id} +
+
+
+ {data ? ( + <> + + + ) : error ? ( +
+

Error Loading Thread

+

{error}

+ +
+ ) : ( + <> +

Loading Thread...

+
+ Loading +
+ + )} +
+ + ); +} +function Head({ node, profile }: { node: FullNode; profile?: UserProfile }) { + return ( + <> + +
+ +
+ + ); +} + +function Minithread({ ff }: { ff: FullFeed }) { + const profiles = useLocalState((s) => s.profiles); + const nodes = Object.values(ff); + return ( +
+ {nodes.map((c) => { + const profile = profiles.get(c.author); + return ( +
+ + +
+ ); + })} +
+ ); +} +function Grandchildren({ node }: { node: FullNode }) { + return ( +
+ +
+ ); +} 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(); + 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 ( <> -- cgit v1.2.3