summaryrefslogtreecommitdiff
path: root/gui/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src/components')
-rw-r--r--gui/src/components/layout/Sidebar.tsx20
-rw-r--r--gui/src/components/modals/UserModal.tsx98
-rw-r--r--gui/src/components/nostr/Feed.tsx1
-rw-r--r--gui/src/components/nostr/User.tsx128
-rw-r--r--gui/src/components/post/Header.tsx4
-rw-r--r--gui/src/components/profile/Profile.tsx52
-rw-r--r--gui/src/components/trill/User.tsx180
7 files changed, 414 insertions, 69 deletions
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() {
<h3> Nostrill </h3>
</div>
<h3>Feeds</h3>
- <div className="opt" role="link" onClick={() => goto(`/feed/global`)}>
+ <div className="opt" role="link" onClick={() => goto(`/f/global`)}>
<Icon name="home" size={20} />
<div>Home</div>
</div>
- <div className="opt notification-item" role="link" onClick={openNotifications}>
+ <div
+ className="opt notification-item"
+ role="link"
+ onClick={openNotifications}
+ >
<div className="notification-icon-wrapper">
<Icon name="bell" size={20} />
{unreadNotifications > 0 && (
@@ -50,7 +54,7 @@ function SlidingMenu() {
<div
className="opt tbd"
role="link"
- // onClick={() => goto("/chat")}
+ // onClick={() => setModal(<p>lmao</p>)}
>
<Icon name="messages" size={20} />
<div>Messages</div>
@@ -63,7 +67,7 @@ function SlidingMenu() {
<div
className="opt"
role="link"
- onClick={() => goto(`/feed/${api!.airlock.our}`)}
+ onClick={() => goto(`/u/${api!.airlock.our}`)}
>
<Icon name="profile" size={20} />
<div>Profile</div>
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 }) {
<div className="user-modal">
{/* Banner Image */}
{bannerImage && (
- <div className="user-modal-banner">
+ <div className="user-banner">
<img src={bannerImage} alt="Profile banner" />
</div>
)}
@@ -174,7 +181,9 @@ export default function ({ userString }: { userString: string }) {
<span className="badge badge-nostr">Nostr</span>
)}
{itsMe && <span className="badge badge-me">You</span>}
- {isFollower && !itsMe && <span className="badge badge-follows">Follows you</span>}
+ {isFollower && !itsMe && (
+ <span className="badge badge-follows">Follows you</span>
+ )}
</div>
</div>
</div>
@@ -213,7 +222,11 @@ export default function ({ userString }: { userString: string }) {
onClick={(e) => e.stopPropagation()}
>
{value}
- <Icon name="nostr" size={12} className="external-link-icon" />
+ <Icon
+ name="nostr"
+ size={12}
+ className="external-link-icon"
+ />
</a>
) : (
<span className="field-value">{value}</span>
@@ -235,21 +248,20 @@ export default function ({ userString }: { userString: string }) {
{loading ? "..." : isFollowing ? "Following" : "Follow"}
</button>
)}
+ <>
+ <button
+ className="action-btn secondary"
+ onClick={() => {
+ navigate(`/u/${userString}`);
+ close();
+ }}
+ >
+ <Icon name="home" size={16} />
+ View Feed
+ </button>
+ </>
- {"urbit" in user ? (
- <>
- <button
- className="action-btn secondary"
- onClick={() => {
- navigate(`/feed/${userString}`);
- close();
- }}
- >
- <Icon name="home" size={16} />
- View Feed
- </button>
- </>
- ) : (
+ {"nostr" in user ? (
<button
className="action-btn secondary"
onClick={handleAvatarClick}
@@ -257,7 +269,7 @@ export default function ({ userString }: { userString: string }) {
<Icon name="nostr" size={16} />
View on Primal
</button>
- )}
+ ) : null}
</div>
</div>
</Modal>
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<FC>();
+
+ // 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 (
+ <>
+ <div className="user-actions">
+ <button
+ onClick={handleFollow}
+ disabled={isFollowLoading}
+ className={`action-btn ${!!feed ? "" : "follow"}`}
+ >
+ {isFollowLoading ? (
+ <>
+ <Icon name="settings" size={16} />
+ {!!feed ? "Unfollowing..." : "Following..."}
+ </>
+ ) : (
+ <>
+ <Icon name={!!feed ? "bell" : "pals"} size={16} />
+ {!!feed ? "Unfollow" : "Follow"}
+ </>
+ )}
+ </button>
+
+ {(!feed || !feed.feed || Object.keys(feed.feed).length === 0) && (
+ <button
+ onClick={handleRequestAccess}
+ disabled={isAccessLoading}
+ className="action-btn access"
+ >
+ {isAccessLoading ? (
+ <>
+ <Icon name="settings" size={16} />
+ Fetching...
+ </>
+ ) : (
+ <>
+ <Icon name="key" size={16} />
+ Fetch Feed
+ </>
+ )}
+ </button>
+ )}
+ </div>
+ {(feed || fc) && (
+ <div id="feed-proper">
+ <Composer />
+ <PostList data={(feed || fc)!} refetch={refetch} />
+ </div>
+ )}
+ </>
+ );
+}
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> = (props) => {
profiles: s.profiles,
}));
const profile = profiles.get(props.userString);
+ console.log({ profiles });
if (props.isMe) return <ProfileEditor {...props} profile={profile} />;
else return <Profile profile={profile} {...props} />;
@@ -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 (
- <div className="profile view-mode">
- <div className="profile-picture">
- <Avatar
- user={user}
- userString={userString}
- size={120}
- picOnly={true}
- profile={profile}
- />
+ <div className="profile">
+ {bannerImage && (
+ <div className="user-banner">
+ <img src={bannerImage} alt="Profile banner" />
+ </div>
+ )}
+ <div className="flex items-center gap-4">
+ <div className="profile-picture">
+ <Avatar
+ user={user}
+ userString={userString}
+ size={120}
+ picOnly={true}
+ profile={profile}
+ />
+ </div>
+ <h2 className="text-4xl">{profile?.name || userString}</h2>
</div>
<div className="profile-info">
- <h2>{profile?.name || userString}</h2>
{profile?.about && <p className="profile-about">{profile.about}</p>}
{customFields.length > 0 && (
<div className="profile-custom-fields">
<h4>Additional Info</h4>
- {customFields.map(([key, value], index) => (
- <div key={index} className="custom-field-view">
- <span className="field-key">{key}:</span>
- <span className="field-value">{value}</span>
- </div>
- ))}
+
+ {customFields.map(([key, value], index) => {
+ if (key.toLocaleLowerCase() === "banner") return null;
+ const isURL = URL.parse(value);
+ return (
+ <div key={index} className="custom-field-view">
+ <span className="field-key">{key}:</span>
+ {isURL ? (
+ <a className="field-value" href={value} target="_blank">
+ {value}
+ </a>
+ ) : (
+ <span className="field-value">{value}</span>
+ )}
+ </div>
+ );
+ })}
</div>
)}
</div>
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<FC>();
+
+ 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 (
+ <>
+ <div className="user-actions">
+ <button
+ onClick={handleFollow}
+ disabled={isFollowLoading}
+ className={`action-btn ${!!feed ? "" : "follow"}`}
+ >
+ {isFollowLoading ? (
+ <>
+ <Icon name="settings" size={16} />
+ {!!feed ? "Unfollowing..." : "Following..."}
+ </>
+ ) : (
+ <>
+ <Icon name={!!feed ? "bell" : "pals"} size={16} />
+ {!!feed ? "Unfollow" : "Follow"}
+ </>
+ )}
+ </button>
+
+ <button
+ onClick={handleRequestAccess}
+ disabled={isAccessLoading}
+ className="action-btn access"
+ >
+ {isAccessLoading ? (
+ <>
+ <Icon name="settings" size={16} />
+ Requesting...
+ </>
+ ) : (
+ <>
+ <Icon name="key" size={16} />
+ Request Access
+ </>
+ )}
+ </button>
+ </div>
+
+ {feed && hasFeed ? (
+ <div id="feed-proper">
+ <Composer />
+ <PostList data={feed} refetch={refetch} />
+ </div>
+ ) : fc ? (
+ <div id="feed-proper">
+ <Composer />
+ <PostList data={fc} refetch={refetch} />
+ </div>
+ ) : null}
+
+ {!feed && !fc && (
+ <div id="other-user-feed">
+ <div className="empty-feed-message">
+ <Icon name="messages" size={48} color="textMuted" />
+ <h3>No Posts Available</h3>
+ <p>
+ This user's posts are not publicly visible.
+ {!!feed && " Try following them"} or request temporary access to
+ see their content.
+ </p>
+ </div>
+ </div>
+ )}
+ </>
+ );
+}
+
+export default UserFeed;