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/App.tsx | 4 +- gui/src/Router.tsx | 27 ++-- 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 ++++++++++++++++++++++++++ gui/src/logic/nostr.ts | 14 ++ gui/src/pages/Error.tsx | 60 +++++++++ gui/src/pages/Feed.tsx | 15 +-- gui/src/pages/Thread.tsx | 2 +- gui/src/pages/User.tsx | 219 ++++++++------------------------ gui/src/state/state.ts | 7 +- gui/src/styles/ErrorPage.css | 156 +++++++++++++++++++++++ gui/src/styles/Profile.css | 17 +++ gui/src/styles/UserModal.css | 46 ++++++- gui/src/styles/styles.css | 1 - 19 files changed, 776 insertions(+), 275 deletions(-) create mode 100644 gui/src/components/nostr/User.tsx create mode 100644 gui/src/components/trill/User.tsx create mode 100644 gui/src/pages/Error.tsx create mode 100644 gui/src/styles/ErrorPage.css diff --git a/gui/src/App.tsx b/gui/src/App.tsx index 415cb66..28398e4 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -14,9 +14,8 @@ const queryClient = new QueryClient(); function App() { const [loading, setLoading] = useState(true); console.log("NOSTRILL INIT"); - const { init, modal } = useLocalState((s) => ({ + const { init } = useLocalState((s) => ({ init: s.init, - modal: s.modal, })); useEffect(() => { init().then((_res: any) => { @@ -36,7 +35,6 @@ function App() { {/* {isMobile ? : } */} - {modal && modal} diff --git a/gui/src/Router.tsx b/gui/src/Router.tsx index ee3aa0d..5026ef0 100644 --- a/gui/src/Router.tsx +++ b/gui/src/Router.tsx @@ -1,12 +1,14 @@ import Sidebar from "@/components/layout/Sidebar"; - -// new +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 { Switch, Router, Redirect, Route } from "wouter"; +import { P404 } from "./pages/Error"; export default function r() { + const modal = useLocalState((s) => s.modal); return ( @@ -14,26 +16,17 @@ export default function r() {
- - + + + +
+ {modal && modal}
); } function toGlobal() { - return ; -} - -export function P404() { - return

404

; -} -export function ErrorPage({ msg }: { msg: string }) { - return ( -
- -

{msg}

-
- ); + return ; } 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; diff --git a/gui/src/logic/nostr.ts b/gui/src/logic/nostr.ts index 7da9b91..3a9a586 100644 --- a/gui/src/logic/nostr.ts +++ b/gui/src/logic/nostr.ts @@ -21,6 +21,20 @@ export function generateNprofile(pubkey: string) { const nprofile = nip19.nprofileEncode(prof); return nprofile; } +export function isValidNostrKey(key: string): boolean { + try { + nip19.decode(key); + return true; + } catch (e) { + try { + nip19.npubEncode(key); + return true; + } catch (e2) { + console.error(e2, "not valid nostr key"); + return false; + } + } +} // let sk = generateSecretKey() // let nsec = nip19.nsecEncode(sk) diff --git a/gui/src/pages/Error.tsx b/gui/src/pages/Error.tsx new file mode 100644 index 0000000..c29e6a6 --- /dev/null +++ b/gui/src/pages/Error.tsx @@ -0,0 +1,60 @@ +import "@/styles/ErrorPage.css"; +import Icon from "@/components/Icon"; +import { Link } from "wouter"; +export function P404() { + return ( +
+
+
+ +
+

404

+

Page Not Found

+

+ The page you're looking for doesn't exist or has been moved. +

+
+ + + + + + +
+
+
+ ); +} + +export function ErrorPage({ msg }: { msg: string }) { + return ( +
+ +

{msg}

+
+
+
+ +
+

Oops!

+

Something went wrong

+

{msg}

+
+ + + +
+
+
+
+ ); +} diff --git a/gui/src/pages/Feed.tsx b/gui/src/pages/Feed.tsx index 02f7b1a..bb001d4 100644 --- a/gui/src/pages/Feed.tsx +++ b/gui/src/pages/Feed.tsx @@ -1,35 +1,30 @@ -// import spinner from "@/assets/icons/spinner.svg"; import "@/styles/trill.css"; import "@/styles/feed.css"; -import UserLoader from "./User"; import PostList from "@/components/feed/PostList"; import useLocalState from "@/state/state"; import { useParams } from "wouter"; import spinner from "@/assets/triangles.svg"; import { useState } from "react"; import Composer from "@/components/composer/Composer"; -import { ErrorPage } from "@/Router"; +import { ErrorPage } from "@/pages/Error"; import NostrFeed from "@/components/nostr/Feed"; type FeedType = "global" | "following" | "nostr"; function Loader() { - // const { api } = useLocalState(); const params = useParams(); console.log({ params }); - // const [loc, navigate] = useLocation(); - // console.log({ loc }); - // const our = api!.airlock.ship; + if (!params.taip) return ; if (params.taip === "global") return ; + if (params.taip === "following") return ; if (params.taip === "nostr") return ; // else if (param === FeedType.Rumors) return ; // else if (param === FeedType.Home) return ; - else if (params.taip) return ; else return ; } function FeedPage({ t }: { t: FeedType }) { const [active, setActive] = useState(t); return ( -
+ <>
) : null}
-
+ ); } // {active === "global" ? ( diff --git a/gui/src/pages/Thread.tsx b/gui/src/pages/Thread.tsx index dec8946..fc215f2 100644 --- a/gui/src/pages/Thread.tsx +++ b/gui/src/pages/Thread.tsx @@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import useLocalState from "@/state/state"; import Icon from "@/components/Icon"; import spinner from "@/assets/triangles.svg"; -import { ErrorPage } from "@/Router"; +import { ErrorPage } from "@/pages/Error"; import "@/styles/trill.css"; import "@/styles/feed.css"; import Post from "@/components/post/Post"; diff --git a/gui/src/pages/User.tsx b/gui/src/pages/User.tsx index b73cd96..1611037 100644 --- a/gui/src/pages/User.tsx +++ b/gui/src/pages/User.tsx @@ -9,204 +9,85 @@ import { useEffect, useState } from "react"; import type { FC } from "@/types/trill"; import type { UserType } from "@/types/nostrill"; import { isValidPatp } from "urbit-ob"; -import { isValidNostrPubkey } from "@/logic/nostrill"; -import { ErrorPage } from "@/Router"; - -function UserLoader({ userString }: { userString: string }) { - const { api, pubkey } = useLocalState((s) => ({ - api: s.api, - pubkey: s.pubkey, - })); - // auto updating on SSE doesn't work if we do shallow - - const user = isValidPatp(userString) - ? { urbit: userString } - : isValidNostrPubkey(userString) - ? { nostr: userString } - : { error: "" }; - - const isOwnProfile = - "urbit" in user - ? user.urbit === api?.airlock.our - : "nostr" in user - ? pubkey === user.nostr - : false; - if ("error" in user) return ; - else - return ; +import { ErrorPage } from "@/pages/Error"; +import { useParams } from "wouter"; +import { isValidNostrKey } from "@/logic/nostr"; +import TrillFeed from "@/components/trill/User"; +import NostrFeed from "@/components/nostr/User"; + +function UserLoader() { + const params = useParams(); + console.log({ params }); + const userString = params.user; + if (!userString) return ; + else if (isValidPatp(userString)) + return ; + else if (isValidNostrKey(userString)) + return ; + else return ; } function UserFeed({ user, userString, - isMe, }: { user: UserType; userString: string; - isMe: boolean; }) { - const { api, addProfile, addNotification, lastFact } = useLocalState((s) => ({ + const { api, pubkey } = useLocalState((s) => ({ api: s.api, addProfile: s.addProfile, addNotification: s.addNotification, lastFact: s.lastFact, + pubkey: s.pubkey, })); + const isMe = + "urbit" in user + ? user.urbit === api?.airlock.our + : "nostr" in user + ? pubkey === user.nostr + : false; // auto updating on SSE doesn't work if we do shallow const { following } = useStore(); const feed = following.get(userString); const hasFeed = !feed ? false : Object.entries(feed).length > 0; const refetch = () => feed; - const isFollowing = following.has(userString); const [isFollowLoading, setIsFollowLoading] = useState(false); const [isAccessLoading, setIsAccessLoading] = useState(false); - 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 (isFollowing) { - await api.unfollow(user); - } else { - await api.follow(user); - toast.success(`Follow request sent to ${userString}`); - } - } catch (error) { - toast.error( - `Failed to ${isFollowing ? "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 (
- - {!isMe && ( -
- - - -
- )} - - {feed && hasFeed ? ( -
- - -
- ) : fc ? ( -
- - -
+ {isMe ? ( + + ) : "urbit" in user ? ( + + ) : "nostr" in user ? ( + ) : null} - - {!isMe && !feed && !fc && ( -
-
- -

No Posts Available

-

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

-
-
- )}
); } export default UserLoader; + +function MyFeed() { + return <>; +} diff --git a/gui/src/state/state.ts b/gui/src/state/state.ts index 7d433f4..69633e3 100644 --- a/gui/src/state/state.ts +++ b/gui/src/state/state.ts @@ -91,7 +91,12 @@ export const useStore = creator((set, get) => ({ } } if ("nostr" in data.fact) { - set({ nostrFeed: data.fact.nostr }); + if ("feed" in data.fact.nostr) + set({ nostrFeed: data.fact.nostr.feed }); + if ("relays" in data.fact.nostr) + set({ relays: data.fact.nostr.relays }); + // if ("user" in data.fact.nostr) + // if ("thread" in data.fact.nostr) } } }); diff --git a/gui/src/styles/ErrorPage.css b/gui/src/styles/ErrorPage.css new file mode 100644 index 0000000..0d04810 --- /dev/null +++ b/gui/src/styles/ErrorPage.css @@ -0,0 +1,156 @@ +/* Error Page Styles */ + +.error-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + width: 100%; + padding: 40px 20px; + background: var(--color-background); +} + +.error-content { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + max-width: 600px; + width: 100%; + padding: 60px 40px; + background: var(--color-surface); + border-radius: 16px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + animation: fadeInUp 0.5s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.error-icon-wrapper { + margin-bottom: 32px; + opacity: 0.6; + animation: float 3s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } +} + +.error-title { + margin: 0 0 16px 0; + font-size: 72px; + font-weight: 700; + color: var(--color-primary); + line-height: 1; + letter-spacing: -2px; +} + +.error-subtitle { + margin: 0 0 16px 0; + font-size: 28px; + font-weight: 600; + color: var(--color-text); +} + +.error-message { + margin: 0 0 40px 0; + font-size: 16px; + line-height: 1.6; + color: var(--color-text-secondary); + max-width: 400px; +} + +.error-actions { + display: flex; + gap: 16px; + flex-wrap: wrap; + justify-content: center; +} + +.error-actions a { + text-decoration: none; +} + +.error-btn { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 14px 28px; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + +.error-btn.primary { + background: var(--color-primary); + color: white; +} + +.error-btn.primary:hover { + background: var(--color-primary-hover); + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} + +.error-btn.secondary { + background: transparent; + color: var(--color-text); + border: 2px solid var(--color-border); +} + +.error-btn.secondary:hover { + background: var(--color-surface-hover); + border-color: var(--color-text-secondary); + transform: translateY(-2px); +} + +.error-btn:active { + transform: translateY(0); +} + +/* Responsive adjustments */ +@media (max-width: 480px) { + .error-content { + padding: 40px 24px; + } + + .error-title { + font-size: 56px; + } + + .error-subtitle { + font-size: 22px; + } + + .error-message { + font-size: 14px; + } + + .error-actions { + flex-direction: column; + width: 100%; + } + + .error-btn { + width: 100%; + justify-content: center; + } +} diff --git a/gui/src/styles/Profile.css b/gui/src/styles/Profile.css index 624cb12..58aefb8 100644 --- a/gui/src/styles/Profile.css +++ b/gui/src/styles/Profile.css @@ -322,4 +322,21 @@ color: var(--color-text-secondary); line-height: 1.5; max-width: 400px; +} + + +/* Banner Image */ +.user-banner { + width: 100%; + height: 160px; + overflow: hidden; + margin: 0; + flex-shrink: 0; +} + +.user-banner img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; } \ No newline at end of file diff --git a/gui/src/styles/UserModal.css b/gui/src/styles/UserModal.css index bf4ff56..e976b38 100644 --- a/gui/src/styles/UserModal.css +++ b/gui/src/styles/UserModal.css @@ -6,7 +6,8 @@ gap: 20px; min-width: 400px; max-width: 500px; - padding: 24px; + padding: 0; + overflow: hidden; } .user-modal-error { @@ -25,6 +26,17 @@ display: flex; gap: 16px; align-items: flex-start; + padding: 24px 24px 0 24px; + margin-top: -40px; + /* Pull avatar up over banner */ + position: relative; + z-index: 1; +} + +/* Reset margin if no banner */ +.user-modal>.user-modal-header:first-child { + margin-top: 0; + padding: 24px; } .user-modal-avatar-wrapper { @@ -36,8 +48,10 @@ height: 80px; border-radius: 50%; overflow: hidden; - border: 3px solid var(--color-border); + border: 4px solid var(--color-background); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); transition: transform 0.2s ease, border-color 0.2s ease; + background: var(--color-background); } .user-modal-avatar-wrapper .avatar:hover { @@ -135,6 +149,7 @@ /* About Section */ .user-modal-about { padding: 16px; + margin: 0 24px; background: var(--color-surface); border-radius: 8px; border-left: 3px solid var(--color-primary); @@ -153,6 +168,7 @@ display: flex; gap: 24px; padding: 16px; + margin: 0 24px; background: var(--color-surface); border-radius: 8px; } @@ -180,6 +196,7 @@ /* Custom Fields */ .user-modal-custom-fields { padding: 16px; + margin: 0 24px; background: var(--color-surface); border-radius: 8px; } @@ -216,11 +233,32 @@ flex: 1; } +.custom-field-item .field-link { + color: var(--color-primary); + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 4px; + transition: all 0.2s ease; + word-break: break-all; +} + +.custom-field-item .field-link:hover { + color: var(--color-primary-hover); + text-decoration: underline; +} + +.external-link-icon { + flex-shrink: 0; + opacity: 0.7; +} + /* Action Buttons */ .user-modal-actions { display: flex; gap: 12px; - padding-top: 8px; + padding: 16px 24px 24px 24px; + margin-top: 8px; border-top: 1px solid var(--color-border); } @@ -313,4 +351,4 @@ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); max-height: 90vh; overflow-y: auto; -} +} \ No newline at end of file diff --git a/gui/src/styles/styles.css b/gui/src/styles/styles.css index 5772c40..41b3c4d 100644 --- a/gui/src/styles/styles.css +++ b/gui/src/styles/styles.css @@ -1,5 +1,4 @@ @import "tailwindcss"; -@import "./UserModal.css"; /* assets */ /* fonts */ -- cgit v1.2.3