diff options
| author | polwex <polwex@sortug.com> | 2025-11-19 05:47:30 +0700 |
|---|---|---|
| committer | polwex <polwex@sortug.com> | 2025-11-19 05:47:30 +0700 |
| commit | 74d84cb2f22600b6246343e9ea606cf0db7517f0 (patch) | |
| tree | 0d68285c8e74e6543645e17ab2751d543c1ff9a6 /gui/src/components/nostr | |
| parent | e6e657be3a3b1dae426b46f3bc16f9a5cf4861c2 (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.tsx | 53 | ||||
| -rw-r--r-- | gui/src/components/nostr/Thread.tsx | 174 | ||||
| -rw-r--r-- | gui/src/components/nostr/User.tsx | 62 |
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 ( <> |
