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/bun.lock | 3 + gui/package.json | 1 + gui/src/Router.tsx | 7 +- gui/src/components/Avatar.tsx | 4 +- gui/src/components/composer/Composer.tsx | 77 ++++++---- gui/src/components/feed/PostList.tsx | 13 +- gui/src/components/layout/Sidebar.tsx | 2 +- gui/src/components/modals/UserModal.tsx | 121 ++++++++-------- gui/src/components/nostr/Feed.tsx | 53 +++++-- gui/src/components/nostr/Thread.tsx | 174 ++++++++++++++++++++++ gui/src/components/nostr/User.tsx | 62 ++++---- gui/src/components/post/Body.tsx | 82 +++++++---- gui/src/components/post/Footer.tsx | 18 ++- gui/src/components/post/Header.tsx | 7 +- gui/src/components/post/Post.tsx | 13 +- gui/src/components/post/PostWrapper.tsx | 14 -- gui/src/components/post/wrappers/Nostr.tsx | 14 +- gui/src/components/profile/Profile.tsx | 15 +- gui/src/components/trill/Thread.tsx | 219 ++++++++++++++++++++++++++++ gui/src/components/trill/User.tsx | 71 ++++----- gui/src/logic/cache.ts | 222 +++++++++++++++++++++++++++++ gui/src/logic/constants.ts | 12 +- gui/src/logic/hooks.ts | 173 ++++++++++++++++++++++ gui/src/logic/nostr.ts | 30 ++-- gui/src/logic/nostrill.ts | 155 +++++++++++++++++--- gui/src/logic/requests/nostrill.ts | 65 ++++++--- gui/src/logic/trill/helpers.ts | 186 +++++++++++++++++++++++- gui/src/pages/Feed.tsx | 56 +++----- gui/src/pages/Thread.tsx | 187 +++++------------------- gui/src/pages/User.tsx | 40 +++--- gui/src/state/state.ts | 52 ++++--- gui/src/styles/Composer.css | 173 ++++++++++++++++++++++ gui/src/styles/UserModal.css | 6 +- gui/src/styles/feed.css | 6 +- gui/src/styles/styles.css | 200 +++----------------------- gui/src/styles/trill.css | 14 ++ gui/src/types/nostrill.ts | 43 +++++- gui/src/types/notifications.ts | 8 +- gui/src/types/trill.ts | 1 + gui/src/types/ui.ts | 2 +- 40 files changed, 1897 insertions(+), 704 deletions(-) create mode 100644 gui/src/components/nostr/Thread.tsx delete mode 100644 gui/src/components/post/PostWrapper.tsx create mode 100644 gui/src/components/trill/Thread.tsx create mode 100644 gui/src/logic/cache.ts create mode 100644 gui/src/logic/hooks.ts create mode 100644 gui/src/styles/Composer.css diff --git a/gui/bun.lock b/gui/bun.lock index 33af6d1..d34d11c 100644 --- a/gui/bun.lock +++ b/gui/bun.lock @@ -7,6 +7,7 @@ "@tailwindcss/vite": "^4.1.14", "@tanstack/react-query": "^5.85.9", "any-ascii": "^0.3.3", + "lucide-react": "^0.554.0", "nostr-tools": "^2.17.2", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -496,6 +497,8 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lucide-react": ["lucide-react@0.554.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA=="], + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], diff --git a/gui/package.json b/gui/package.json index 9ab8472..30baccb 100644 --- a/gui/package.json +++ b/gui/package.json @@ -13,6 +13,7 @@ "@tailwindcss/vite": "^4.1.14", "@tanstack/react-query": "^5.85.9", "any-ascii": "^0.3.3", + "lucide-react": "^0.554.0", "nostr-tools": "^2.17.2", "react": "^19.1.1", "react-dom": "^19.1.1", diff --git a/gui/src/Router.tsx b/gui/src/Router.tsx index 5026ef0..5566524 100644 --- a/gui/src/Router.tsx +++ b/gui/src/Router.tsx @@ -3,7 +3,7 @@ import useLocalState from "@/state/state"; import Feed from "@/pages/Feed"; import User from "@/pages/User"; import Settings from "@/pages/Settings"; -import Thread from "@/pages/Thread"; +import Thread, { NostrThreadLoader } from "@/pages/Thread"; import { Switch, Router, Redirect, Route } from "wouter"; import { P404 } from "./pages/Error"; @@ -14,12 +14,13 @@ export default function r() {
- + +
{modal && modal}
@@ -27,6 +28,6 @@ export default function r() { ); } -function toGlobal() { +function toMain() { return ; } diff --git a/gui/src/components/Avatar.tsx b/gui/src/components/Avatar.tsx index 2b38848..664bc90 100644 --- a/gui/src/components/Avatar.tsx +++ b/gui/src/components/Avatar.tsx @@ -7,7 +7,6 @@ import UserModal from "./modals/UserModal"; export default function ({ user, - userString, size, color, noClickOnName, @@ -24,7 +23,6 @@ export default function ({ }) { const { setModal } = useLocalState((s) => ({ setModal: s.setModal })); // TODO revisit this when %whom updates - console.log({ profile }); const avatarInner = profile ? ( ) : "urbit" in user && isValidPatp(user.urbit) ? ( @@ -43,7 +41,7 @@ export default function ({ function openModal(e: React.MouseEvent) { if (noClickOnName) return; e.stopPropagation(); - setModal(); + setModal(); } const name = (
diff --git a/gui/src/components/composer/Composer.tsx b/gui/src/components/composer/Composer.tsx index 8b7e343..1fb0c6c 100644 --- a/gui/src/components/composer/Composer.tsx +++ b/gui/src/components/composer/Composer.tsx @@ -1,10 +1,13 @@ +import "@/styles/Composer.css"; import useLocalState from "@/state/state"; +import spinner from "@/assets/triangles.svg"; import Sigil from "@/components/Sigil"; import { useState, useEffect, useRef, type FormEvent } from "react"; import Snippets, { ReplySnippet } from "./Snippets"; import toast from "react-hot-toast"; import Icon from "@/components/Icon"; import { wait } from "@/logic/utils"; +import type { UserType } from "@/types/nostrill"; function Composer({ isAnon }: { isAnon?: boolean }) { const { api, composerData, addNotification, setComposerData } = useLocalState( @@ -15,12 +18,12 @@ function Composer({ isAnon }: { isAnon?: boolean }) { setComposerData: s.setComposerData, }), ); - const our = api!.airlock.our!; const [input, setInput] = useState(""); const [isExpanded, setIsExpanded] = useState(false); const [isLoading, setLoading] = useState(false); const inputRef = useRef(null); + console.log({ composerData }); useEffect(() => { if (composerData) { setIsExpanded(true); @@ -29,8 +32,6 @@ function Composer({ isAnon }: { isAnon?: boolean }) { composerData.post && "trill" in composerData.post ) { - const author = composerData.post.trill.author; - setInput(`${author} `); } // Auto-focus input when composer opens setTimeout(() => { @@ -38,27 +39,46 @@ function Composer({ isAnon }: { isAnon?: boolean }) { }, 100); // Small delay to ensure the composer is rendered } }, [composerData]); + async function addSimple() { + if (!api) return; // TODOhandle error + return await api.addPost(input); + } + async function addComplex() { + if (!api) return; // TODOhandle error + if (!composerData) return; + const host: UserType = + "trill" in composerData.post + ? { urbit: composerData.post.trill.author } + : "nostr" in composerData.post + ? { nostr: composerData.post.nostr.pubkey } + : { urbit: api.airlock.our! }; + const id = + "trill" in composerData.post + ? composerData.post.trill.id + : "nostr" in composerData.post + ? composerData.post.nostr.eventId + : ""; + const thread = + "trill" in composerData.post + ? composerData.post.trill.thread || composerData.post.trill.id + : "nostr" in composerData.post + ? composerData.post.nostr.eventId + : ""; + + const res = + composerData.type === "reply" + ? api.addReply(input, host, id, thread) + : composerData?.type === "quote" + ? api.addQuote(input, host, id) + : wait(500); + return await res; + } async function poast(e: FormEvent) { e.preventDefault(); - // TODO + if (!api) return; // TODOhandle error + const our = api.airlock.our!; setLoading(true); - - const res = - composerData?.type === "reply" && "trill" in composerData.post - ? api!.addReply( - input, - composerData.post.trill.host, - composerData.post.trill.id, - composerData.post.trill.thread || composerData.post.trill.id, - ) - : composerData?.type === "quote" && "trill" in composerData.post - ? api!.addQuote(input, { - ship: composerData.post.trill.host, - id: composerData.post.trill.id, - }) - : !composerData - ? api!.addPost(input) - : wait(500); + const res = !composerData ? addSimple() : addComplex(); const ares = await res; if (ares) { // // Check for mentions in the post (ship names starting with ~) @@ -95,6 +115,7 @@ function Composer({ isAnon }: { isAnon?: boolean }) { setInput(""); setComposerData(null); // Clear composer data after successful post toast.success("post sent"); + setLoading(false); setIsExpanded(false); } } @@ -121,7 +142,7 @@ function Composer({ isAnon }: { isAnon?: boolean }) { onSubmit={poast} >
- +
@@ -172,9 +193,15 @@ function Composer({ isAnon }: { isAnon?: boolean }) { onFocus={() => setIsExpanded(true)} placeholder={placeHolder} /> - + {isLoading ? ( +
+ +
+ ) : ( + + )}
{/* Quote snippets appear below input */} diff --git a/gui/src/components/feed/PostList.tsx b/gui/src/components/feed/PostList.tsx index 12b58b4..3659bde 100644 --- a/gui/src/components/feed/PostList.tsx +++ b/gui/src/components/feed/PostList.tsx @@ -1,6 +1,8 @@ import TrillPost from "@/components/post/Post"; import type { FC } from "@/types/trill"; import useLocalState from "@/state/state"; +import type { UserType } from "@/types/nostrill"; +import { isValidPatp } from "urbit-ob"; // import { useEffect } from "react"; // import { useQueryClient } from "@tanstack/react-query"; // import { toFull } from "../thread/helpers"; @@ -26,14 +28,21 @@ function TrillFeed({ data, refetch }: { data: FC; refetch: Function }) { .filter((i) => !data.feed[i].parent) .sort() .reverse() - .slice(0, 50) + // .slice(0, 50) .map((i) => { const poast = data.feed[i]; - const profile = profiles.get(poast.author); + const user: UserType = poast.event + ? { nostr: poast.event.pubkey } + : isValidPatp(poast.author) + ? { urbit: poast.author } + : { nostr: poast.author }; + const userString = "urbit" in user ? user.urbit : user.nostr; + const profile = profiles.get(userString); return ( diff --git a/gui/src/components/layout/Sidebar.tsx b/gui/src/components/layout/Sidebar.tsx index bc11e48..12087c1 100644 --- a/gui/src/components/layout/Sidebar.tsx +++ b/gui/src/components/layout/Sidebar.tsx @@ -30,7 +30,7 @@ function SlidingMenu() {

Nostrill

Feeds

-
goto(`/f/global`)}> +
goto(`/f`)}>
Home
diff --git a/gui/src/components/modals/UserModal.tsx b/gui/src/components/modals/UserModal.tsx index aeffc95..4a7f812 100644 --- a/gui/src/components/modals/UserModal.tsx +++ b/gui/src/components/modals/UserModal.tsx @@ -6,13 +6,12 @@ import Icon from "@/components/Icon"; import useLocalState from "@/state/state"; import { useLocation } from "wouter"; import toast from "react-hot-toast"; -import { isValidPatp } from "urbit-ob"; -import { isValidNostrPubkey } from "@/logic/nostrill"; import { generateNprofile } from "@/logic/nostr"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import type { UserType } from "@/types/nostrill"; -export default function ({ userString }: { userString: string }) { - const { setModal, api, pubkey, profiles, following, followers } = +export default function ({ user }: { user: UserType }) { + const { setModal, api, lastFact, pubkey, profiles, following, followers } = useLocalState((s) => ({ setModal: s.setModal, api: s.api, @@ -20,31 +19,15 @@ export default function ({ userString }: { userString: string }) { profiles: s.profiles, following: s.following, followers: s.followers, + lastFact: s.lastFact, })); const [_, navigate] = useLocation(); - const [loading, setLoading] = useState(false); + const [isLoading, setLoading] = useState(false); function close() { setModal(null); } - const user = isValidPatp(userString) - ? { urbit: userString } - : isValidNostrPubkey(userString) - ? { nostr: userString } - : { error: "" }; - - if ("error" in user) { - return ( - -
- -

Invalid user identifier

-
-
- ); - } - const itsMe = "urbit" in user ? user.urbit === api?.airlock.our @@ -52,6 +35,7 @@ export default function ({ userString }: { userString: string }) { ? user.nostr === pubkey : false; + const userString = "urbit" in user ? user.urbit : user.nostr; const profile = profiles.get(userString); const isFollowing = following.has(userString); const isFollower = followers.includes(userString); @@ -60,6 +44,15 @@ export default function ({ userString }: { userString: string }) { const userFeed = following.get(userString); const postCount = userFeed ? Object.keys(userFeed.feed).length : 0; + // useEffect(() => { + // if (!lastFact) return; + // if (!("fols" in lastFact)) return; + // if (!("new" in lastFact.fols)) return; + // if (lastFact.fols.new.user === userString) setLoading(false); + // const name = profile?.name || userString; + // toast.success(`Followed ${name}`); + // }, [lastFact]); + async function copy(e: React.MouseEvent) { e.stopPropagation(); await navigator.clipboard.writeText(userString); @@ -93,7 +86,7 @@ export default function ({ userString }: { userString: string }) { } catch (err) { toast.error("Action failed"); } finally { - setLoading(false); + // setLoading(false); } } @@ -112,16 +105,6 @@ export default function ({ userString }: { userString: string }) { ? `${userString.slice(0, 10)}...${userString.slice(-8)}` : userString; - // Check if a string is a URL - const isURL = (str: string): boolean => { - try { - new URL(str); - return true; - } catch { - return str.startsWith("http://") || str.startsWith("https://"); - } - }; - // Get banner image from profile.other const bannerImage = profile?.other?.banner || profile?.other?.Banner; @@ -210,29 +193,15 @@ export default function ({ userString }: { userString: string }) { {otherFields.length > 0 && (

Additional Info

- {otherFields.map(([key, value]) => ( -
- {key}: - {isURL(value) ? ( - e.stopPropagation()} - > - {value} - - - ) : ( - {value} - )} -
- ))} + {otherFields.map(([key, value]) => { + console.log({ key, value }); + return ( +
+ {key}: + +
+ ); + })}
)} @@ -242,10 +211,10 @@ export default function ({ userString }: { userString: string }) { )} <> @@ -275,3 +244,37 @@ export default function ({ userString }: { userString: string }) { ); } + +// Check if a string is a URL +const isURL = (str: string): boolean => { + if (!str) return false; + try { + new URL(str); + return true; + } catch { + return false; + } +}; + +export function ProfValue({ value }: { value: any }) { + if (typeof value === "string") + return isURL(value) ? ( + e.stopPropagation()} + > + {value} + + + ) : ( + {value} + ); + else if (typeof value === "number") + return {value}; + else if (typeof value === "object") + return {JSON.stringify(value)}; + else return {JSON.stringify(value)}; +} 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 ( <> diff --git a/gui/src/components/post/Body.tsx b/gui/src/components/post/Body.tsx index b4f1bb2..ca5aa6e 100644 --- a/gui/src/components/post/Body.tsx +++ b/gui/src/components/post/Body.tsx @@ -6,7 +6,6 @@ import type { 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"; @@ -15,6 +14,7 @@ import Quote from "./Quote"; import PostData from "./Loader"; import Card from "./Card.tsx"; import type { Ship } from "@/types/urbit.ts"; +import { extractURLs } from "@/logic/nostrill.ts"; function Body(props: PostProps) { const text = props.poast.contents.filter((c) => { @@ -95,41 +95,63 @@ function TextBlock({ block }: { block: Block }) { ) ) : 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 ? ( - {i.text} - ) : "italic" in i ? ( - {i.italic} - ) : "bold" in i ? ( - {i.bold} - ) : "strike" in i ? ( - {i.strike} - ) : "underline" in i ? ( - {i.underline} - ) : "sup" in i ? ( - {i.sup} - ) : "sub" in i ? ( - {i.sub} - ) : "ship" in i ? ( - gotoShip(e, i.ship)} - > - {i.ship} - - ) : "codespan" in i ? ( - {i.codespan} - ) : "link" in i ? ( - - ) : "break" in i ? ( -
- ) : null; + if ("text" in i) { + const tokens = extractURLs(i.text); + return ( + <> + {tokens.text.map((t, i) => + "text" in t ? ( + {t.text} + ) : ( + + {t.link.show} + + ), + )} + {tokens.pics.map((p, i) => ( + + ))} + {tokens.vids.map((p, i) => ( +