From 420a543f8af3075502b0a7530a0fa06af264eb8b Mon Sep 17 00:00:00 2001 From: polwex Date: Tue, 18 Nov 2025 16:59:24 +0700 Subject: refactoring gui too, improvements to Nostr user logic --- gui/src/components/layout/Sidebar.tsx | 20 ++-- gui/src/components/modals/UserModal.tsx | 98 +++++++++-------- gui/src/components/nostr/Feed.tsx | 1 + gui/src/components/nostr/User.tsx | 128 +++++++++++++++++++++++ gui/src/components/post/Header.tsx | 4 +- gui/src/components/profile/Profile.tsx | 52 ++++++--- gui/src/components/trill/User.tsx | 180 ++++++++++++++++++++++++++++++++ 7 files changed, 414 insertions(+), 69 deletions(-) create mode 100644 gui/src/components/nostr/User.tsx create mode 100644 gui/src/components/trill/User.tsx (limited to 'gui/src/components') diff --git a/gui/src/components/layout/Sidebar.tsx b/gui/src/components/layout/Sidebar.tsx index c267e2f..bc11e48 100644 --- a/gui/src/components/layout/Sidebar.tsx +++ b/gui/src/components/layout/Sidebar.tsx @@ -7,16 +7,16 @@ import { ThemeSwitcher } from "@/styles/ThemeSwitcher"; function SlidingMenu() { const [_, navigate] = useLocation(); - const { api, unreadNotifications, setModal } = useLocalState((s) => ({ + const { api, unreadNotifications, setModal } = useLocalState((s) => ({ api: s.api, unreadNotifications: s.unreadNotifications, - setModal: s.setModal + setModal: s.setModal, })); - + function goto(to: string) { navigate(to); } - + function openNotifications() { // We'll create this component next import("../NotificationCenter").then(({ default: NotificationCenter }) => { @@ -30,11 +30,15 @@ function SlidingMenu() {

Nostrill

Feeds

-
goto(`/feed/global`)}> +
goto(`/f/global`)}>
Home
-
+
{unreadNotifications > 0 && ( @@ -50,7 +54,7 @@ function SlidingMenu() {
goto("/chat")} + // onClick={() => setModal(

lmao

)} >
Messages
@@ -63,7 +67,7 @@ function SlidingMenu() {
goto(`/feed/${api!.airlock.our}`)} + onClick={() => goto(`/u/${api!.airlock.our}`)} >
Profile
diff --git a/gui/src/components/modals/UserModal.tsx b/gui/src/components/modals/UserModal.tsx index 0694f1e..aeffc95 100644 --- a/gui/src/components/modals/UserModal.tsx +++ b/gui/src/components/modals/UserModal.tsx @@ -1,3 +1,5 @@ +import "@/styles/Profile.css"; +import "@/styles/UserModal.css"; import Modal from "./Modal"; import Avatar from "../Avatar"; import Icon from "@/components/Icon"; @@ -10,14 +12,15 @@ import { generateNprofile } from "@/logic/nostr"; import { useState } from "react"; export default function ({ userString }: { userString: string }) { - const { setModal, api, pubkey, profiles, following, followers } = useLocalState((s) => ({ - setModal: s.setModal, - api: s.api, - pubkey: s.pubkey, - profiles: s.profiles, - following: s.following, - followers: s.followers, - })); + const { setModal, api, pubkey, profiles, following, followers } = + useLocalState((s) => ({ + setModal: s.setModal, + api: s.api, + pubkey: s.pubkey, + profiles: s.profiles, + following: s.following, + followers: s.followers, + })); const [_, navigate] = useLocation(); const [loading, setLoading] = useState(false); @@ -64,25 +67,28 @@ export default function ({ userString }: { userString: string }) { } async function handleFollow(e: React.MouseEvent) { + if ("error" in user) return; e.stopPropagation(); if (!api) return; setLoading(true); try { if (isFollowing) { - const result = await api.unfollow(userString); - if ("ok" in result) { - toast.success(`Unfollowed ${profile?.name || userString}`); - } else { - toast.error(result.error); - } + const result = await api.unfollow(user); + console.log(result); + // if ("ok" in result) { + // toast.success(`Unfollowed ${profile?.name || userString}`); + // } else { + // toast.error(result.error); + // } } else { - const result = await api.follow(userString); - if ("ok" in result) { - toast.success(`Following ${profile?.name || userString}`); - } else { - toast.error(result.error); - } + const result = await api.follow(user); + console.log(result); + // if ("ok" in result) { + // toast.success(`Following ${profile?.name || userString}`); + // } else { + // toast.error(result.error); + // } } } catch (err) { toast.error("Action failed"); @@ -101,9 +107,10 @@ export default function ({ userString }: { userString: string }) { } const displayName = profile?.name || ("urbit" in user ? user.urbit : "Anon"); - const truncatedId = userString.length > 20 - ? `${userString.slice(0, 10)}...${userString.slice(-8)}` - : userString; + const truncatedId = + userString.length > 20 + ? `${userString.slice(0, 10)}...${userString.slice(-8)}` + : userString; // Check if a string is a URL const isURL = (str: string): boolean => { @@ -111,7 +118,7 @@ export default function ({ userString }: { userString: string }) { new URL(str); return true; } catch { - return str.startsWith('http://') || str.startsWith('https://'); + return str.startsWith("http://") || str.startsWith("https://"); } }; @@ -121,7 +128,7 @@ export default function ({ userString }: { userString: string }) { // Filter out banner from other fields since we display it separately const otherFields = profile?.other ? Object.entries(profile.other).filter( - ([key]) => key.toLowerCase() !== 'banner' + ([key]) => key.toLowerCase() !== "banner", ) : []; @@ -130,7 +137,7 @@ export default function ({ userString }: { userString: string }) {
{/* Banner Image */} {bannerImage && ( -
+
Profile banner
)} @@ -174,7 +181,9 @@ export default function ({ userString }: { userString: string }) { Nostr )} {itsMe && You} - {isFollower && !itsMe && Follows you} + {isFollower && !itsMe && ( + Follows you + )}
@@ -213,7 +222,11 @@ export default function ({ userString }: { userString: string }) { onClick={(e) => e.stopPropagation()} > {value} - + ) : ( {value} @@ -235,21 +248,20 @@ export default function ({ userString }: { userString: string }) { {loading ? "..." : isFollowing ? "Following" : "Follow"} )} + <> + + - {"urbit" in user ? ( - <> - - - ) : ( + {"nostr" in user ? ( - )} + ) : null}
diff --git a/gui/src/components/nostr/Feed.tsx b/gui/src/components/nostr/Feed.tsx index 0e74cea..d21307b 100644 --- a/gui/src/components/nostr/Feed.tsx +++ b/gui/src/components/nostr/Feed.tsx @@ -14,6 +14,7 @@ export default function Nostr() { })); console.log({ relays }); const [isSyncing, setIsSyncing] = useState(false); + console.log({ nostrFeed }); const feed = eventsToFc(nostrFeed); console.log({ feed }); const refetch = () => feed; diff --git a/gui/src/components/nostr/User.tsx b/gui/src/components/nostr/User.tsx new file mode 100644 index 0000000..a9e9e2f --- /dev/null +++ b/gui/src/components/nostr/User.tsx @@ -0,0 +1,128 @@ +import useLocalState from "@/state/state"; +import { 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"; + +export default function NostrUser({ + user, + userString, + feed, + isFollowLoading, + setIsFollowLoading, + isAccessLoading, + setIsAccessLoading, +}: { + user: UserType; + userString: string; + feed: FC | undefined; + isFollowLoading: boolean; + setIsFollowLoading: (b: boolean) => void; + isAccessLoading: boolean; + setIsAccessLoading: (b: boolean) => void; +}) { + const { api } = useLocalState((s) => ({ + api: s.api, + })); + const [fc, setFC] = useState(); + + // Show empty state with resync option when no feed data + + async function refetch() { + // + } + async function handleFollow() { + if (!api) return; + + setIsFollowLoading(true); + try { + if (feed) { + await api.unfollow(user); + } else { + await api.follow(user); + toast.success(`Follow request sent to ${userString}`); + } + } catch (error) { + toast.error(`Failed to ${!!feed ? "unfollow" : "follow"} ${userString}`); + setIsFollowLoading(false); + console.error("Follow error:", error); + } + } + async function handleRequestAccess() { + 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); + // } + } + return ( + <> +
+ + + {(!feed || !feed.feed || Object.keys(feed.feed).length === 0) && ( + + )} +
+ {(feed || fc) && ( +
+ + +
+ )} + + ); +} diff --git a/gui/src/components/post/Header.tsx b/gui/src/components/post/Header.tsx index b0822b4..5898eba 100644 --- a/gui/src/components/post/Header.tsx +++ b/gui/src/components/post/Header.tsx @@ -8,12 +8,12 @@ function Header(props: PostProps) { // console.log(props.poast.author.length, "length"); function go(e: React.MouseEvent) { e.stopPropagation(); - navigate(`/feed/${poast.host}`); + navigate(`/u/${poast.host}`); } function openThread(e: React.MouseEvent) { e.stopPropagation(); const sel = window.getSelection()?.toString(); - if (!sel) navigate(`/feed/${poast.host}/${poast.id}`); + if (!sel) navigate(`/t/${poast.host}/${poast.id}`); } const { poast } = props; const name = profile ? ( diff --git a/gui/src/components/profile/Profile.tsx b/gui/src/components/profile/Profile.tsx index b5f22e9..ab65a7b 100644 --- a/gui/src/components/profile/Profile.tsx +++ b/gui/src/components/profile/Profile.tsx @@ -16,6 +16,7 @@ const Loader: React.FC = (props) => { profiles: s.profiles, })); const profile = profiles.get(props.userString); + console.log({ profiles }); if (props.isMe) return ; else return ; @@ -32,31 +33,50 @@ function Profile({ // Initialize state with existing profile or defaults // View-only mode for other users' profiles - no editing allowed + const bannerImage = profile?.other?.banner || profile?.other?.Banner; const customFields = profile?.other ? Object.entries(profile.other) : []; return ( -
-
- +
+ {bannerImage && ( +
+ Profile banner +
+ )} +
+
+ +
+

{profile?.name || userString}

-

{profile?.name || userString}

{profile?.about &&

{profile.about}

} {customFields.length > 0 && (

Additional Info

- {customFields.map(([key, value], index) => ( -
- {key}: - {value} -
- ))} + + {customFields.map(([key, value], index) => { + if (key.toLocaleLowerCase() === "banner") return null; + const isURL = URL.parse(value); + return ( +
+ {key}: + {isURL ? ( + + {value} + + ) : ( + {value} + )} +
+ ); + })}
)}
diff --git a/gui/src/components/trill/User.tsx b/gui/src/components/trill/User.tsx new file mode 100644 index 0000000..b7b53d6 --- /dev/null +++ b/gui/src/components/trill/User.tsx @@ -0,0 +1,180 @@ +// import spinner from "@/assets/icons/spinner.svg"; +import Composer from "@/components/composer/Composer"; +import PostList from "@/components/feed/PostList"; +import useLocalState from "@/state/state"; +import Icon from "@/components/Icon"; +import toast from "react-hot-toast"; +import { useEffect, useState } from "react"; +import type { FC } from "@/types/trill"; +import type { UserType } from "@/types/nostrill"; + +function UserFeed({ + user, + userString, + feed, + isFollowLoading, + setIsFollowLoading, + isAccessLoading, + setIsAccessLoading, +}: { + user: UserType; + userString: string; + feed: FC | undefined; + isFollowLoading: boolean; + setIsFollowLoading: (b: boolean) => void; + isAccessLoading: boolean; + setIsAccessLoading: (b: boolean) => void; +}) { + const { api, addProfile, addNotification, lastFact } = useLocalState((s) => ({ + api: s.api, + addProfile: s.addProfile, + addNotification: s.addNotification, + lastFact: s.lastFact, + })); + const hasFeed = !feed ? false : Object.entries(feed).length > 0; + const refetch = () => feed; + + const [fc, setFC] = useState(); + + useEffect(() => { + console.log("fact", lastFact); + console.log(isFollowLoading); + if (!isFollowLoading) return; + const follow = lastFact?.fols; + if (!follow) return; + if ("new" in follow) { + if (userString !== follow.new.user) return; + toast.success(`Now following ${userString}`); + setIsFollowLoading(false); + addNotification({ + type: "follow", + from: userString, + message: `You are now following ${userString}`, + }); + } else if ("quit" in follow) { + toast.success(`Unfollowed ${userString}`); + setIsFollowLoading(false); + addNotification({ + type: "unfollow", + from: userString, + message: `You unfollowed ${userString}`, + }); + } + }, [lastFact, userString, isFollowLoading]); + + const handleFollow = async () => { + if (!api) return; + + setIsFollowLoading(true); + try { + if (!!feed) { + await api.unfollow(user); + } else { + await api.follow(user); + toast.success(`Follow request sent to ${userString}`); + } + } catch (error) { + toast.error(`Failed to ${!!feed ? "unfollow" : "follow"} ${userString}`); + setIsFollowLoading(false); + console.error("Follow error:", error); + } + }; + + const handleRequestAccess = async () => { + if (!api) return; + if (!("urbit" in user)) 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); + } + }; + console.log({ user, userString, feed, fc }); + + return ( + <> +
+ + + +
+ + {feed && hasFeed ? ( +
+ + +
+ ) : fc ? ( +
+ + +
+ ) : null} + + {!feed && !fc && ( +
+
+ +

No Posts Available

+

+ This user's posts are not publicly visible. + {!!feed && " Try following them"} or request temporary access to + see their content. +

+
+
+ )} + + ); +} + +export default UserFeed; -- cgit v1.2.3