summaryrefslogtreecommitdiff
path: root/front
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-09-18 00:24:39 +0700
committerpolwex <polwex@sortug.com>2025-09-18 00:24:39 +0700
commit4b016c908dda2019f3bf89e5a3d2eae535e5fbd2 (patch)
tree639613aa8bcc3d36b5165a32ece4a421dabde4c8 /front
parent985fa2f7c99832cdf3c3351d2273c8fd05402b78 (diff)
oioi
Diffstat (limited to 'front')
-rw-r--r--front/src/Router.tsx8
-rw-r--r--front/src/components/Avatar.tsx26
-rw-r--r--front/src/components/Icon.tsx10
-rw-r--r--front/src/components/modals/UserModal.tsx65
-rw-r--r--front/src/components/post/Post.tsx1
-rw-r--r--front/src/components/profile/Editor.tsx (renamed from front/src/components/ProfileEditor.tsx)104
-rw-r--r--front/src/components/profile/Profile.tsx67
-rw-r--r--front/src/logic/api.ts2
-rw-r--r--front/src/logic/nostrill.ts21
-rw-r--r--front/src/logic/requests/nostrill.ts16
-rw-r--r--front/src/pages/Feed.tsx10
-rw-r--r--front/src/pages/User.tsx85
-rw-r--r--front/src/state/state.ts14
-rw-r--r--front/src/styles/Profile.css (renamed from front/src/styles/ProfileEditor.css)2
-rw-r--r--front/src/styles/feed.css7
-rw-r--r--front/src/types/nostrill.ts1
16 files changed, 318 insertions, 121 deletions
diff --git a/front/src/Router.tsx b/front/src/Router.tsx
index 83d212f..1293709 100644
--- a/front/src/Router.tsx
+++ b/front/src/Router.tsx
@@ -27,3 +27,11 @@ function toGlobal() {
export function P404() {
return <h1 className="x-center">404</h1>;
}
+export function ErrorPage({ msg }: { msg: string }) {
+ return (
+ <div>
+ <P404 />
+ <h3>{msg}</h3>
+ </div>
+ );
+}
diff --git a/front/src/components/Avatar.tsx b/front/src/components/Avatar.tsx
index 0f3dc90..a071655 100644
--- a/front/src/components/Avatar.tsx
+++ b/front/src/components/Avatar.tsx
@@ -1,20 +1,21 @@
import useLocalState from "@/state/state";
-import type { Ship } from "@/types/urbit";
import Sigil from "./Sigil";
-import ShipModal from "./modals/ShipModal";
import { isValidPatp } from "urbit-ob";
-import type { UserProfile } from "@/types/nostrill";
+import type { UserProfile, UserType } from "@/types/nostrill";
import Icon from "@/components/Icon";
+import UserModal from "./modals/UserModal";
export default function ({
- p,
+ user,
+ userString,
size,
color,
noClickOnName,
profile,
picOnly = false,
}: {
- p: Ship;
+ user: UserType;
+ userString: string;
size: number;
color?: string;
noClickOnName?: boolean;
@@ -23,10 +24,11 @@ export default function ({
}) {
const { setModal } = useLocalState((s) => ({ setModal: s.setModal }));
// TODO revisit this when %whom updates
+ console.log({ profile });
const avatarInner = profile ? (
- <img src={profile.picture} />
- ) : isValidPatp(p) ? (
- <Sigil patp={p} size={size} bg={color} />
+ <img src={profile.picture} width={size} height={size} />
+ ) : "urbit" in user && isValidPatp(user.urbit) ? (
+ <Sigil patp={user.urbit} size={size} bg={color} />
) : (
<Icon name="comet" />
);
@@ -41,14 +43,18 @@ export default function ({
function openModal(e: React.MouseEvent) {
if (noClickOnName) return;
e.stopPropagation();
- setModal(<ShipModal ship={p} />);
+ setModal(<UserModal user={user} userString={userString} />);
}
const name = (
<div className="name cp" role="link" onMouseUp={openModal}>
{profile ? (
<p>{profile.name}</p>
+ ) : "urbit" in user ? (
+ <p className={"p-only" + tooLong(user.urbit)}>
+ {user.urbit.length > 28 ? "Anon" : user.urbit}
+ </p>
) : (
- <p className={"p-only" + tooLong(p)}>{p.length > 28 ? "Anon" : p}</p>
+ <p className={"p-only" + tooLong(user.nostr)}>{user.nostr}</p>
)}
</div>
);
diff --git a/front/src/components/Icon.tsx b/front/src/components/Icon.tsx
index a316e08..797a87b 100644
--- a/front/src/components/Icon.tsx
+++ b/front/src/components/Icon.tsx
@@ -65,7 +65,7 @@ interface IconProps {
size?: number;
className?: string;
title?: string;
- onClick?: (e?: React.MouseEvent) => void;
+ onClick?: (e: React.MouseEvent) => any;
color?: "primary" | "text" | "textSecondary" | "textMuted" | "custom";
customColor?: string;
}
@@ -84,7 +84,11 @@ const Icon: React.FC<IconProps> = ({
// Simple filter based on theme - icons should match text
const getFilter = () => {
// For dark themes, invert the black SVGs to white
- if (theme.name === "dark" || theme.name === "noir" || theme.name === "gruvbox") {
+ if (
+ theme.name === "dark" ||
+ theme.name === "noir" ||
+ theme.name === "gruvbox"
+ ) {
return "invert(1)";
}
// For light themes with dark text, keep as is
@@ -130,4 +134,4 @@ const Icon: React.FC<IconProps> = ({
);
};
-export default Icon; \ No newline at end of file
+export default Icon;
diff --git a/front/src/components/modals/UserModal.tsx b/front/src/components/modals/UserModal.tsx
new file mode 100644
index 0000000..6e3089d
--- /dev/null
+++ b/front/src/components/modals/UserModal.tsx
@@ -0,0 +1,65 @@
+import Modal from "./Modal";
+import Avatar from "../Avatar";
+import Icon from "@/components/Icon";
+import useLocalState from "@/state/state";
+import { useLocation } from "wouter";
+import toast from "react-hot-toast";
+import type { UserType } from "@/types/nostrill";
+
+export default function ({
+ user,
+ userString,
+}: {
+ user: UserType;
+ userString: string;
+}) {
+ const { setModal, api, pubkey } = useLocalState((s) => ({
+ setModal: s.setModal,
+ api: s.api,
+ pubkey: s.pubkey,
+ }));
+ const [_, navigate] = useLocation();
+ function close() {
+ setModal(null);
+ }
+ const itsMe =
+ "urbit" in user
+ ? user.urbit === api?.airlock.our
+ : "nostr" in user
+ ? user.nostr === pubkey
+ : false;
+ async function copy(e: React.MouseEvent) {
+ e.stopPropagation();
+ await navigator.clipboard.writeText(userString);
+ toast.success("Copied to clipboard");
+ }
+ return (
+ <Modal close={close}>
+ <div id="ship-modal">
+ <div className="flex">
+ <Avatar user={user} userString={userString} size={60} />
+ <Icon
+ name="copy"
+ size={20}
+ className="copy-icon cp"
+ onClick={copy}
+ title="Copy ship name"
+ />
+ </div>
+ <div className="buttons f1">
+ <button onClick={() => navigate(`/feed/${userString}`)}>Feed</button>
+ <button onClick={() => navigate(`/pals/${userString}`)}>
+ Profile
+ </button>
+ {itsMe && (
+ <>
+ <button onClick={() => navigate(`/chat/dm/${userString}`)}>
+ DM
+ </button>
+ </>
+ )}
+ </div>
+ </div>
+ </Modal>
+ );
+}
diff --git a/front/src/components/post/Post.tsx b/front/src/components/post/Post.tsx
index 277c119..2965040 100644
--- a/front/src/components/post/Post.tsx
+++ b/front/src/components/post/Post.tsx
@@ -22,6 +22,7 @@ export interface PostProps {
profile?: UserProfile;
}
function Post(props: PostProps) {
+ console.log("post", props);
const { poast } = props;
if (!poast || poast.contents === null) {
return null;
diff --git a/front/src/components/ProfileEditor.tsx b/front/src/components/profile/Editor.tsx
index 9a7493f..2e4aebc 100644
--- a/front/src/components/ProfileEditor.tsx
+++ b/front/src/components/profile/Editor.tsx
@@ -1,31 +1,37 @@
-import { useState, useEffect } from "react";
-import type { UserProfile } from "@/types/nostrill";
+import { useState } from "react";
+import type { UserProfile, UserType } from "@/types/nostrill";
import useLocalState from "@/state/state";
import Icon from "@/components/Icon";
import toast from "react-hot-toast";
-import Avatar from "./Avatar";
+import Avatar from "../Avatar";
interface ProfileEditorProps {
- ship: string;
+ user: UserType;
+ userString: string;
+ profile: UserProfile | undefined;
onSave?: () => void;
}
-const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => {
+const ProfileEditor: React.FC<ProfileEditorProps> = ({
+ user,
+ profile,
+ userString,
+ onSave,
+}) => {
const { api, profiles } = useLocalState((s) => ({
api: s.api,
+ pubkey: s.pubkey,
profiles: s.profiles,
}));
- const isOwnProfile = ship === api?.airlock.our;
// Initialize state with existing profile or defaults
- const existingProfile = profiles.get(ship);
- const [name, setName] = useState(existingProfile?.name || "");
- const [picture, setPicture] = useState(existingProfile?.picture || "");
- const [about, setAbout] = useState(existingProfile?.about || "");
+ const [name, setName] = useState(profile?.name || userString);
+ const [picture, setPicture] = useState(profile?.picture || "");
+ const [about, setAbout] = useState(profile?.about || "");
const [customFields, setCustomFields] = useState<
Array<{ key: string; value: string }>
>(
- Object.entries(existingProfile?.other || {}).map(([key, value]) => ({
+ Object.entries(profile?.other || {}).map(([key, value]) => ({
key,
value,
})),
@@ -33,21 +39,6 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => {
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
- useEffect(() => {
- const profile = profiles.get(ship);
- if (profile) {
- setName(profile.name || "");
- setPicture(profile.picture || "");
- setAbout(profile.about || "");
- setCustomFields(
- Object.entries(profile.other || {}).map(([key, value]) => ({
- key,
- value,
- })),
- );
- }
- }, [ship, profiles]);
-
const handleAddCustomField = () => {
setCustomFields([...customFields, { key: "", value: "" }]);
};
@@ -77,7 +68,7 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => {
}
});
- const profile: UserProfile = {
+ const nprofile: UserProfile = {
name,
picture,
about,
@@ -86,7 +77,7 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => {
// Call API to save profile
if (api && typeof api.createProfile === "function") {
- await api.createProfile(profile);
+ await api.createProfile(nprofile);
} else {
throw new Error("Profile update API not available");
}
@@ -104,9 +95,9 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => {
const handleCancel = () => {
// Reset to original values
- const profile = profiles.get(ship);
+ const profile = profiles.get(userString);
if (profile) {
- setName(profile.name || "");
+ setName(profile.name || userString);
setPicture(profile.picture || "");
setAbout(profile.about || "");
setCustomFields(
@@ -118,39 +109,14 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => {
}
setIsEditing(false);
};
-
- if (!isOwnProfile) {
- // View-only mode for other users' profiles - no editing allowed
- return (
- <div className="profile-editor view-mode">
- <div className="profile-picture">
- <Avatar p={ship} size={120} picOnly={true} />
- </div>
- <div className="profile-info">
- <h2>{name || ship}</h2>
- {about && <p className="profile-about">{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>
- ))}
- </div>
- )}
- </div>
- </div>
- );
- }
+ console.log({ profile });
+ console.log({ name, picture, customFields });
return (
<div className="profile-editor">
<div className="profile-header">
<h2>Edit Profile</h2>
- {isOwnProfile && !isEditing && (
+ {!isEditing && (
<button onClick={() => setIsEditing(true)} className="edit-btn">
<Icon name="settings" size={16} />
Edit
@@ -181,7 +147,17 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => {
placeholder="https://example.com/avatar.jpg"
/>
<div className="picture-preview">
- <Avatar p={ship} size={54} picOnly={true} />
+ {picture ? (
+ <img src={picture} />
+ ) : (
+ <Avatar
+ user={user}
+ userString={userString}
+ profile={profile}
+ size={120}
+ picOnly={true}
+ />
+ )}
</div>
</div>
@@ -252,11 +228,17 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => {
) : (
<div className="profile-view">
<div className="profile-picture">
- <Avatar p={ship} size={120} picOnly={true} />
+ <Avatar
+ user={user}
+ userString={userString}
+ profile={profile}
+ size={120}
+ picOnly={true}
+ />
</div>
<div className="profile-info">
- <h3>{name || ship}</h3>
+ <h3>{name}</h3>
{about && <p className="profile-about">{about}</p>}
{customFields.length > 0 && (
diff --git a/front/src/components/profile/Profile.tsx b/front/src/components/profile/Profile.tsx
new file mode 100644
index 0000000..b5f22e9
--- /dev/null
+++ b/front/src/components/profile/Profile.tsx
@@ -0,0 +1,67 @@
+import "@/styles/Profile.css";
+import type { UserProfile, UserType } from "@/types/nostrill";
+import useLocalState from "@/state/state";
+import Avatar from "../Avatar";
+import ProfileEditor from "./Editor";
+
+interface Props {
+ user: UserType;
+ userString: string;
+ isMe: boolean;
+ onSave?: () => void;
+}
+
+const Loader: React.FC<Props> = (props) => {
+ const { profiles } = useLocalState((s) => ({
+ profiles: s.profiles,
+ }));
+ const profile = profiles.get(props.userString);
+
+ if (props.isMe) return <ProfileEditor {...props} profile={profile} />;
+ else return <Profile profile={profile} {...props} />;
+};
+function Profile({
+ user,
+ userString,
+ profile,
+}: {
+ user: UserType;
+ userString: string;
+ profile: UserProfile | undefined;
+}) {
+ // Initialize state with existing profile or defaults
+
+ // View-only mode for other users' profiles - no editing allowed
+ 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>
+ <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>
+ ))}
+ </div>
+ )}
+ </div>
+ </div>
+ );
+}
+
+export default Loader;
diff --git a/front/src/logic/api.ts b/front/src/logic/api.ts
index cf44073..148d255 100644
--- a/front/src/logic/api.ts
+++ b/front/src/logic/api.ts
@@ -1,6 +1,6 @@
import Urbit from "urbit-api";
-export const URL = import.meta.env.PROD ? "" : "http://localhost:8083";
+export const URL = import.meta.env.PROD ? "" : "http://localhost:8081";
export async function start(): Promise<Urbit> {
const airlock = new Urbit(URL, "");
diff --git a/front/src/logic/nostrill.ts b/front/src/logic/nostrill.ts
index bf9212d..bd5fc9c 100644
--- a/front/src/logic/nostrill.ts
+++ b/front/src/logic/nostrill.ts
@@ -1,6 +1,9 @@
import type { Event } from "@/types/nostr";
import type { Content, FC, Poast } from "@/types/trill";
import { engagementBunt, openLock } from "./bunts";
+import type { UserType } from "@/types/nostrill";
+import type { Result } from "@/types/ui";
+import { isValidPatp } from "urbit-ob";
export function eventsToFc(postEvents: Event[]): FC {
const fc = postEvents.reduce(
(acc: FC, event: Event) => {
@@ -66,6 +69,24 @@ export function eventToPoast(event: Event): Poast | null {
return poast;
}
+export function userToString(user: UserType): Result<string> {
+ if ("urbit" in user) {
+ const isValid = isValidPatp(user.urbit);
+ if (isValid) return { ok: user.urbit };
+ else return { error: "invalid @p" };
+ } else if ("nostr" in user) return { ok: user.nostr };
+ else return { error: "unknown user" };
+}
+export function isValidNostrPubkey(pubkey: string): boolean {
+ // TODO
+ if (pubkey.length !== 64) return false;
+ try {
+ BigInt("0x" + pubkey);
+ return true;
+ } catch (_e) {
+ return false;
+ }
+}
// NOTE common tags:
// imeta
// client
diff --git a/front/src/logic/requests/nostrill.ts b/front/src/logic/requests/nostrill.ts
index 74fcb87..4147e35 100644
--- a/front/src/logic/requests/nostrill.ts
+++ b/front/src/logic/requests/nostrill.ts
@@ -2,7 +2,7 @@ import type Urbit from "urbit-api";
import type { Cursor, FC, PostID } from "@/types/trill";
import type { Ship } from "@/types/urbit";
import { FeedPostCount } from "../constants";
-import type { UserProfile } from "@/types/nostrill";
+import type { UserProfile, UserType } from "@/types/nostrill";
import type { AsyncRes } from "@/types/ui";
// Subscribe
@@ -121,13 +121,13 @@ export default class IO {
}
// follows
- async follow(ship: Ship) {
- const json = { add: ship };
+ async follow(user: UserType) {
+ const json = { add: user };
return this.poke({ fols: json });
}
- async unfollow(ship: Ship) {
- const json = { del: ship };
+ async unfollow(user: UserType) {
+ const json = { del: user };
return await this.poke({ fols: json });
}
// profiles
@@ -162,7 +162,9 @@ export default class IO {
}
// threads
//
- async peekFeed(host: string): AsyncRes<FC> {
+ async peekFeed(
+ host: string,
+ ): AsyncRes<{ feed: FC; profile: UserProfile | null }> {
try {
const json = { begs: { feed: host } };
const res: any = await this.thread("beg", json);
@@ -170,7 +172,7 @@ export default class IO {
if (!("begs" in res)) return { error: "wrong request" };
if ("ng" in res.begs) return { error: res.begs.ng };
if (!("feed" in res.begs.ok)) return { error: "wrong request" };
- else return { ok: res.begs.ok.feed };
+ else return { ok: res.begs.ok };
} catch (e) {
return { error: `${e}` };
}
diff --git a/front/src/pages/Feed.tsx b/front/src/pages/Feed.tsx
index 5902162..66acc66 100644
--- a/front/src/pages/Feed.tsx
+++ b/front/src/pages/Feed.tsx
@@ -1,7 +1,7 @@
// import spinner from "@/assets/icons/spinner.svg";
import "@/styles/trill.css";
import "@/styles/feed.css";
-import UserFeed from "./User";
+import UserLoader from "./User";
import PostList from "@/components/feed/PostList";
import useLocalState from "@/state/state";
import { useParams } from "wouter";
@@ -10,10 +10,8 @@ import { useState } from "react";
import Composer from "@/components/composer/Composer";
import Icon from "@/components/Icon";
import toast from "react-hot-toast";
-// import UserFeed from "./User";
-import { P404 } from "@/Router";
-import { isValidPatp } from "urbit-ob";
import { eventsToFc } from "@/logic/nostrill";
+import { ErrorPage } from "@/Router";
type FeedType = "global" | "following" | "nostr";
function Loader() {
@@ -27,8 +25,8 @@ function Loader() {
if (params.taip === "nostr") return <FeedPage t={"nostr"} />;
// else if (param === FeedType.Rumors) return <Rumors />;
// else if (param === FeedType.Home) return <UserFeed p={our} />;
- else if (isValidPatp(params.taip!)) return <UserFeed p={params.taip!} />;
- else return <P404 />;
+ else if (params.taip) return <UserLoader userString={params.taip!} />;
+ else return <ErrorPage msg="No such page" />;
}
function FeedPage({ t }: { t: FeedType }) {
const [active, setActive] = useState<FeedType>(t);
diff --git a/front/src/pages/User.tsx b/front/src/pages/User.tsx
index e209bb3..d8b66e1 100644
--- a/front/src/pages/User.tsx
+++ b/front/src/pages/User.tsx
@@ -1,25 +1,59 @@
// import spinner from "@/assets/icons/spinner.svg";
import Composer from "@/components/composer/Composer";
import PostList from "@/components/feed/PostList";
-import ProfileEditor from "@/components/ProfileEditor";
+import Profile from "@/components/profile/Profile";
import useLocalState, { useStore } from "@/state/state";
-import type { Ship } from "@/types/urbit";
-import "@/styles/ProfileEditor.css";
import Icon from "@/components/Icon";
import toast from "react-hot-toast";
import { 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 UserFeed({ p }: { p: Ship }) {
- const { api } = useLocalState((s) => ({
+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 <ErrorPage msg={"Invalid user"} />;
+ else
+ return <UserFeed user={user} userString={userString} isMe={isOwnProfile} />;
+}
+
+function UserFeed({
+ user,
+ userString,
+ isMe,
+}: {
+ user: UserType;
+ userString: string;
+ isMe: boolean;
+}) {
+ const { api, addProfile } = useLocalState((s) => ({
+ api: s.api,
+ addProfile: s.addProfile,
}));
// auto updating on SSE doesn't work if we do shallow
const { following } = useStore();
- const feed = following.get(p);
+ const feed = following.get(userString);
const refetch = () => feed;
- const isOwnProfile = p === api?.airlock.our;
- const isFollowing = following.has(p);
+ const isFollowing = following.has(userString);
const [isFollowLoading, setIsFollowLoading] = useState(false);
const [isAccessLoading, setIsAccessLoading] = useState(false);
@@ -31,14 +65,16 @@ function UserFeed({ p }: { p: Ship }) {
setIsFollowLoading(true);
try {
if (isFollowing) {
- await api.unfollow(p);
- toast.success(`Unfollowed ${p}`);
+ await api.unfollow(user);
+ toast.success(`Unfollowed ${userString}`);
} else {
- await api.follow(p);
- toast.success(`Now following ${p}`);
+ await api.follow(user);
+ toast.success(`Now following ${userString}`);
}
} catch (error) {
- toast.error(`Failed to ${isFollowing ? "unfollow" : "follow"} ${p}`);
+ toast.error(
+ `Failed to ${isFollowing ? "unfollow" : "follow"} ${userString}`,
+ );
console.error("Follow error:", error);
} finally {
setIsFollowLoading(false);
@@ -47,15 +83,20 @@ function UserFeed({ p }: { p: Ship }) {
const handleRequestAccess = async () => {
if (!api) return;
+ if (!("urbit" in user)) return;
setIsAccessLoading(true);
try {
- const res = await api.peekFeed(p);
- toast.success(`Access request sent to ${p}`);
+ const res = await api.peekFeed(user.urbit);
+ toast.success(`Access request sent to ${user.urbit}`);
if ("error" in res) toast.error(res.error);
- else setFC(res.ok);
+ 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 ${p}`);
+ toast.error(`Failed to request access from ${user.urbit}`);
console.error("Access request error:", error);
} finally {
setIsAccessLoading(false);
@@ -64,14 +105,14 @@ function UserFeed({ p }: { p: Ship }) {
return (
<div id="user-page">
- <ProfileEditor ship={p} />
+ <Profile user={user} userString={userString} isMe={isMe} />
- {!isOwnProfile && (
+ {!isMe && (
<div className="user-actions">
<button
onClick={handleFollow}
disabled={isFollowLoading}
- className={`action-btn ${isFollowing ? "following" : "follow"}`}
+ className={`action-btn ${isFollowing ? "" : "follow"}`}
>
{isFollowLoading ? (
<>
@@ -118,7 +159,7 @@ function UserFeed({ p }: { p: Ship }) {
</div>
) : null}
- {!isOwnProfile && !feed && !fc && (
+ {!isMe && !feed && !fc && (
<div id="other-user-feed">
<div className="empty-feed-message">
<Icon name="messages" size={48} color="textMuted" />
@@ -135,4 +176,4 @@ function UserFeed({ p }: { p: Ship }) {
);
}
-export default UserFeed;
+export default UserLoader;
diff --git a/front/src/state/state.ts b/front/src/state/state.ts
index 2e747ea..715427d 100644
--- a/front/src/state/state.ts
+++ b/front/src/state/state.ts
@@ -19,10 +19,11 @@ export type LocalState = {
setModal: (modal: JSX.Element | null) => void;
composerData: ComposerData | null;
setComposerData: (c: ComposerData | null) => void;
- key: string;
+ pubkey: string;
nostrFeed: Event[];
relays: Record<string, Event[]>;
profiles: Map<string, UserProfile>; // pubkey key
+ addProfile: (key: string, u: UserProfile) => void;
following: Map<string, FC>;
followers: string[];
};
@@ -38,7 +39,7 @@ export const useStore = creator((set, get) => ({
await api.subscribeStore((data) => {
console.log("store sub", data);
if ("state" in data) {
- const { feed, nostr, following, relays, profiles, key } = data.state;
+ const { feed, nostr, following, relays, profiles, pubkey } = data.state;
const flwing = new Map(Object.entries(following as Record<string, FC>));
flwing.set(api!.airlock.our!, feed);
set({
@@ -46,7 +47,7 @@ export const useStore = creator((set, get) => ({
nostrFeed: nostr,
profiles: new Map(Object.entries(profiles)),
following: flwing,
- key,
+ pubkey,
});
} else if ("fact" in data) {
if ("post" in data.fact) {
@@ -65,8 +66,13 @@ export const useStore = creator((set, get) => ({
});
set({ api });
},
- key: "",
+ pubkey: "",
profiles: new Map(),
+ addProfile: (key, profile) => {
+ const profiles = get().profiles;
+ profiles.set(key, profile);
+ set({ profiles });
+ },
relays: {},
nostrFeed: [],
following: new Map(),
diff --git a/front/src/styles/ProfileEditor.css b/front/src/styles/Profile.css
index c1b65e5..624cb12 100644
--- a/front/src/styles/ProfileEditor.css
+++ b/front/src/styles/Profile.css
@@ -1,4 +1,4 @@
-.profile-editor {
+.profile {
align-items: center;
padding: 20px;
background: var(--color-surface);
diff --git a/front/src/styles/feed.css b/front/src/styles/feed.css
index 05f0bb2..02d64db 100644
--- a/front/src/styles/feed.css
+++ b/front/src/styles/feed.css
@@ -2,12 +2,6 @@
border: 1px solid var(--color-text);
}
-.avatar,
-.avatar img {
- width: 48px;
- height: 48px;
-}
-
/* Nostr Feed Styles */
.nostr-empty-state {
display: flex;
@@ -133,6 +127,7 @@
from {
transform: rotate(0deg);
}
+
to {
transform: rotate(360deg);
}
diff --git a/front/src/types/nostrill.ts b/front/src/types/nostrill.ts
index bcd3628..5ce033c 100644
--- a/front/src/types/nostrill.ts
+++ b/front/src/types/nostrill.ts
@@ -1,6 +1,7 @@
import type { NostrEvent } from "./nostr";
import type { Poast } from "./trill";
+export type UserType = { urbit: string } | { nostr: string };
export type UserProfile = {
name: string;
picture: string; // URL