summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--desk/app/nostrill.hoon40
-rw-r--r--desk/lib/json/nostrill.hoon70
-rw-r--r--desk/lib/nostrill/comms.hoon41
-rw-r--r--desk/lib/nostrill/follows.hoon40
-rw-r--r--desk/lib/nostrill/mutations.hoon4
-rw-r--r--desk/sur/nostrill.hoon11
-rw-r--r--desk/sur/nostrill/comms.hoon4
-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
23 files changed, 452 insertions, 197 deletions
diff --git a/desk/app/nostrill.hoon b/desk/app/nostrill.hoon
index 17732e7..cbd1c2f 100644
--- a/desk/app/nostrill.hoon
+++ b/desk/app/nostrill.hoon
@@ -1,7 +1,7 @@
/- sur=nostrill, nsur=nostr
/+ lib=nostrill, nlib=nostr, sr=sortug,
shim, dbug, muta=nostrill-mutations, jsonlib=json-nostrill,
- trill=trill-post, comms=nostrill-comms
+ trill=trill-post, comms=nostrill-comms, followlib=follows
/= web /web/router
|%
+$ versioned-state $%(state-0:sur)
@@ -17,6 +17,7 @@
mutat ~(. muta [state bowl])
shimm ~(. shim [state bowl])
coms ~(. comms [state bowl])
+ fols ~(. followlib [state bowl])
++ on-init
^- (quip card:agent:gall agent:gall)
=/ default (default-state:lib bowl)
@@ -31,11 +32,11 @@
++ on-load
|~ old-state=vase
^- (quip card:agent:gall agent:gall)
- =/ old-state !<(versioned-state old-state)
- ?- -.old-state
- %0 `this(state old-state)
- ==
- :: `this(state (default-state:lib bowl))
+ :: =/ old-state !<(versioned-state old-state)
+ :: ?- -.old-state
+ :: %0 `this(state old-state)
+ :: ==
+ `this(state (default-state:lib bowl))
::
++ on-poke
|~ [=mark =vase]
@@ -97,7 +98,7 @@
=/ sp (build-sp:trill our.bowl our.bowl content.poke)
=/ p (build-post:trill now.bowl pub.i.keys sp)
=. state (add-to-feed:mutat p)
- =/ profile (~(get by profiles) pub.i.keys)
+ =/ profile (~(get by profiles) [%urbit our.bowl])
=/ pw [p (some pub.i.keys) ~ ~ profile]
=/ =fact:ui:sur [%post %add pw]
=/ card (update-ui:cards fact)
@@ -116,17 +117,21 @@
[cs this]
==
++ handle-fols |= poke=fols-poke:ui:sur
- ?- -.poke
- %add `this
- %del `this
- ==
+ =^ cs state
+ ?- -.poke
+ %add (handle-add:fols +.poke)
+
+ %del (handle-del:fols +.poke)
+ ==
+ [cs this]
+
++ handle-prof |= poke=prof-poke:ui:sur
?- -.poke
%add
- =. profiles (~(put by profiles) pub.i.keys +.poke)
+ =. profiles (~(put by profiles) [%urbit our.bowl] +.poke)
`this
%del
- =. profiles (~(del by profiles) pub.i.keys)
+ =. profiles (~(del by profiles) [%urbit our.bowl])
`this
==
++ handle-rela |= poke=relay-poke:ui:sur
@@ -243,12 +248,13 @@
|= =(pole knot)
~& on-watch=`path`pole
?+ pole !!
+ [%follow ~] :_ this (give-feed:coms pole)
[%beg %feed ~]
- :_ this give-feed:coms
+ :_ this (give-feed:coms pole)
[%beg %thread ids=@t ~]
=/ id (slaw:sr %uw ids.pole)
?~ id ~& error-parsing-ted-id=pole `this
- :_ this (give-ted:coms u.id)
+ :_ this (give-ted:coms u.id pole)
[%ui ~]
?> .=(our.bowl src.bowl)
:_ this
@@ -271,8 +277,10 @@
==
::
++ on-agent
- |~ [wire sign:agent:gall]
+ |~ [wire=(pole knot) =sign:agent:gall]
^- (quip card:agent:gall agent:gall)
+ ~& on-agent=wire
+ ~& on-agent=sign
`this
::
++ on-arvo
diff --git a/desk/lib/json/nostrill.hoon b/desk/lib/json/nostrill.hoon
index b5a619c..2edf7f4 100644
--- a/desk/lib/json/nostrill.hoon
+++ b/desk/lib/json/nostrill.hoon
@@ -36,35 +36,42 @@
:: TODO do we even need this
:- sub-id (numb received.es)
- ++ en-profiles |= m=(map @ux user-meta:nsur)
+ ++ en-profiles |= m=(map user:sur user-meta:nsur)
%- pairs
- %+ turn ~(tap by m) |= [key=@ux p=user-meta:nsur]
- =/ jkey (hex:en:common key)
+ %+ turn ~(tap by m) |= [key=user:sur p=user-meta:nsur]
+ =/ jkey (user key)
?> ?=(%s -.jkey)
:- +.jkey (user-meta:en:nostr p)
++ enfollowing
- |= m=(map @ux feed:feed)
+ |= m=(map user:sur feed:feed)
^- json
- %- pairs %+ turn ~(tap by m) |= [key=@ux f=feed:feed]
- =/ jkey (hex:en:common key)
+ %- pairs %+ turn ~(tap by m) |= [key=user:sur f=feed:feed]
+ =/ jkey (user key)
?> ?=(%s -.jkey)
:- +.jkey (feed:en:trill f)
++ engraph
- |= m=(map @ux (set follow:sur))
+ |= m=(map user:sur (set user:sur))
^- json
- %- pairs %+ turn ~(tap by m) |= [key=@ux s=(set follow:sur)]
- =/ jkey (hex:en:common key)
+ %- pairs %+ turn ~(tap by m) |= [key=user:sur s=(set user:sur)]
+ =/ jkey (user key)
?> ?=(%s -.jkey)
:- +.jkey
- :- %a %+ turn ~(tap in s) |= f=follow:sur
- %- pairs
- :~ pubkey+(hex:en:common pubkey.f)
- name+s+name.f
- :- %relay ?~ relay.f ~ s+u.relay.f
- ==
+ :- %a %+ turn ~(tap in s) user
+ ++ follow
+ |= f=follow:sur
+ %- pairs
+ :~ pubkey+(hex:en:common pubkey.f)
+ name+s+name.f
+ :- %relay ?~ relay.f ~ s+u.relay.f
+ ==
+ ++ user |= u=user:sur ^- json
+ ?- -.u
+ %urbit (patp:en:common +.u)
+ %nostr (hex:en:common +.u)
+ ==
:: ui facts
++ fact |= f=fact:ui:sur ^- json
%+ frond %fact
@@ -105,17 +112,27 @@
%ng [%s msg.res]
==
++ resd |= rd=res-data:comms ^- json
- %+ frond -.rd
?- -.rd
- %feed (feed-with-cursor:en:trill +.rd)
+ %feed (user-data +.rd)
:: TODO wrap it for nostr shit
- %thread (full-node:en:trill +.rd)
- %prof (user-meta:en:nostr +.rd)
+ %thread (frond -.rd (full-node:en:trill +.rd))
+ ==
+ ++ user-data
+ |= ud=[=fc:feed profile=(unit user-meta:nsur)]
+ %: pairs
+ feed+(feed-with-cursor:en:trill fc.ud)
+ :- %profile ?~ profile.ud ~ (user-meta:en:nostr u.profile.ud)
+ ~
==
--
++ de
=, dejs-soft:format
|%
+++ user
+ %- of :~
+ urbit+(se:de:common %p)
+ nostr+hex:de:common
+ ==
:: ui
++ ui
%- of :~
@@ -128,8 +145,8 @@
==
++ ui-fols
%- of :~
- add+hex:de:common
- del+hex:de:common
+ add+user
+ del+user
==
++ ui-begs
%- of :~
@@ -143,9 +160,18 @@
==
++ ui-prof
%- of :~
- add+user-meta:de:nostr
+ add+ui-meta
del+ul
==
+++ ui-meta
+ %- ot :~
+ name+so
+ about+so
+ picture+so
+ other+other-meta
+ ==
+++ other-meta |= jon=json
+ ?. ?=(%o -.jon) ~ (some p.jon)
++ ui-post
%- of :~
add+de-post
diff --git a/desk/lib/nostrill/comms.hoon b/desk/lib/nostrill/comms.hoon
index 833c07d..87a63b2 100644
--- a/desk/lib/nostrill/comms.hoon
+++ b/desk/lib/nostrill/comms.hoon
@@ -9,7 +9,6 @@
?- -.req
%feed handle-feed
%thread (handle-thread +.req)
- %prof handle-prof
==
++ handle-feed
=/ can (can-access:gatelib src.bowl lock.feed-perms.state bowl)
@@ -21,49 +20,35 @@
=/ lp latest-page:feedlib
=/ lp2 lp(count backlog.feed-perms.state)
=/ =fc:feed (lp2 feed.state)
- =/ crd (res-poke [%ok %feed fc])
+ =/ prof (~(get by profiles.state) [%urbit our.bowl])
+ =/ crd (res-poke [%ok %feed fc prof])
:_ state :~(crd)
-++ give-feed
+++ give-feed
+ |= pat=path
~& give-feed=src.bowl
=/ can (can-access:gatelib src.bowl lock.feed-perms.state bowl)
?. can
:: TODO keep track of the requests at the feed-perms struct
- (res-fact [%ng 'not allowed'])
+ (res-fact [%ng 'not allowed'] pat)
::
=/ lp latest-page:feedlib
=/ lp2 lp(count backlog.feed-perms.state)
=/ =fc:feed (lp2 feed.state)
- (res-fact [%ok %feed fc])
+ =/ prof (~(get by profiles.state) [%urbit our.bowl])
+ (res-fact [%ok %feed fc prof] pat)
-++ give-ted |= id=@
+++ give-ted |= [id=@ pat=path]
=/ ted (get:orm:feed feed.state id)
?~ ted
- (res-fact [%ng 'no such thread'])
+ (res-fact [%ng 'no such thread'] pat)
=/ can (can-access:gatelib src.bowl read.u.ted bowl)
?. can
- (res-fact [%ng 'not allowed'])
+ (res-fact [%ng 'not allowed'] pat)
::
=/ fn (node-to-full:feedlib u.ted feed.state)
- (res-fact [%ok %thread fn])
+ (res-fact [%ok %thread fn] pat)
::
-++ handle-prof
- =/ can (can-access:gatelib src.bowl lock.feed-perms.state bowl)
- ?. can
- :: TODO keep track of the requests at the feed-perms struct
- =/ crd (res-poke [%ng 'not allowed'])
- :_ state :~(crd)
- ::
- :: TODO @p or keys... wat do
- :: =/ up (~(get by profiles.state) our.bowl)
- =/ up (~(get by profiles.state) pub.i.keys.state)
- ?~ up
- =/ crd (res-poke [%ng 'dont have one'])
- :_ state :~(crd)
-
- =/ crd (res-poke [%ok %prof u.up])
- :_ state :~(crd)
-
++ handle-thread |= id=@da
=/ ted (get:orm:feed feed.state id)
?~ ted
@@ -85,8 +70,8 @@
=/ =poke:comms [%res res]
=/ cage [%noun !>(poke)]
[%pass /poke %agent [src.bowl dap.bowl] %poke cage]
-++ res-fact |= =res:comms ^- (list card:agent:gall)
- =/ paths ~[/beg/feed]
+++ res-fact |= [=res:comms pat=path] ^- (list card:agent:gall)
+ =/ paths ~[pat]
=/ =poke:comms [%res res]
~& > giving-res-fact=res
=/ jon (beg-res:en:jsonlib res)
diff --git a/desk/lib/nostrill/follows.hoon b/desk/lib/nostrill/follows.hoon
new file mode 100644
index 0000000..c2eb987
--- /dev/null
+++ b/desk/lib/nostrill/follows.hoon
@@ -0,0 +1,40 @@
+/- sur=nostrill, nsur=nostr, comms=nostrill-comms, feed=trill-feed
+/+ js=json-nostr, sr=sortug, nlib=nostr, constants, gatelib=trill-gate, feedlib=trill-feed, jsonlib=json-nostrill
+|_ [=state:sur =bowl:gall]
+++ handle-add |= =user:sur
+ ^- (quip card:agent:gall _state)
+ ?- -.user
+ %urbit =/ c (urbit-watch +.user)
+ :- :~(c) state
+ %nostr `state
+ ==
+++ handle-del |= =user:sur
+ ^- (quip card:agent:gall _state)
+ =. following.state (~(del by following.state) user)
+ =/ graph (~(get by follow-graph.state) [%urbit our.bowl])
+ ?~ graph `state
+ =/ nset (~(del in u.graph) user)
+ =. follow-graph.state (~(put by follow-graph.state) [%urbit our.bowl] nset)
+ `state
+++ handle-follow-ok |= [=user:sur =feed:feed profile=(unit user-meta:nsur)]
+ ^- (quip card:agent:gall _state)
+ =. following (~(put by following) user feed)
+ =. profiles ?~ profile profiles (~(put by profiles) user u.profile)
+ `state
+
+
+++ urbit-watch |= sip=@p ^- card:agent:gall
+ [%pass /watch %agent [sip dap.bowl] %watch /follow]
+
+:: ++ res-fact |= =res:comms ^- (list card:agent:gall)
+:: =/ paths ~[/beg/feed]
+:: =/ =poke:comms [%res res]
+:: ~& > giving-res-fact=res
+:: =/ jon (beg-res:en:jsonlib res)
+:: =/ cage [%json !>(jon)]
+:: :~
+:: [%give %fact paths cage]
+:: [%give %kick paths ~]
+:: ==
+
+--
diff --git a/desk/lib/nostrill/mutations.hoon b/desk/lib/nostrill/mutations.hoon
index f493bcf..8fca2b2 100644
--- a/desk/lib/nostrill/mutations.hoon
+++ b/desk/lib/nostrill/mutations.hoon
@@ -181,7 +181,7 @@
?~ ujon ~& failed-parse-metadata=ujon `state
=/ umeta (user-meta:de:njs u.ujon)
?~ umeta ~& >> failed-dejs-metadata=ujon `state
- =. profiles.state (~(put by profiles.state) pubkey.event u.umeta)
+ =. profiles.state (~(put by profiles.state) [%nostr pubkey.event] u.umeta)
`state
@@ -224,7 +224,7 @@
`state
++ parse-follow
^- (quip card _state)
- =/ following (~(get by follow-graph.state) pubkey.event)
+ =/ following (~(get by follow-graph.state) [%nostr pubkey.event])
=/ follow-set ?~ following *(set follow:sur) u.following
|- ?~ tags.event `state
=/ t=tag:nsur i.tags.event
diff --git a/desk/sur/nostrill.hoon b/desk/sur/nostrill.hoon
index 70ce480..a091dd0 100644
--- a/desk/sur/nostrill.hoon
+++ b/desk/sur/nostrill.hoon
@@ -12,9 +12,9 @@
:: nostr feed from relays
=nostr-feed
:: profiles
- profiles=(map @ux user-meta:nostr)
- following=(map @ux =feed:trill)
- follow-graph=(map @ux (set follow))
+ profiles=(map user user-meta:nostr)
+ following=(map user =feed:trill)
+ follow-graph=(map user (set user))
:: TODO global feed somehow?
==
@@ -29,6 +29,7 @@ $: pub=(unit @ux)
relay=(unit @t)
pr=(unit user-meta:nostr)
==
++$ user $%([%urbit p=@p] [%nostr p=@ux])
+$ follow [pubkey=@ux name=@t relay=(unit @t)]
++ ui
@@ -52,8 +53,8 @@ $: pub=(unit @ux)
[%del pubkey=@ux]
==
+$ fols-poke
- $% [%add pubkey=@ux]
- [%del pubkey=@ux]
+ $% [%add =user]
+ [%del =user]
==
+$ prof-poke
$% [%add meta=user-meta:nostr]
diff --git a/desk/sur/nostrill/comms.hoon b/desk/sur/nostrill/comms.hoon
index d3dc8e1..4930235 100644
--- a/desk/sur/nostrill/comms.hoon
+++ b/desk/sur/nostrill/comms.hoon
@@ -8,15 +8,13 @@
+$ req
$% [%feed ~]
[%thread id=@da]
- [%prof ~]
==
+$ res
$% [%ok p=res-data]
[%ng msg=@t]
==
+$ res-data
- $% [%feed =fc:feed]
+ $% [%feed =fc:feed profile=(unit user-meta:nsur)]
[%thread p=full-node:post]
- [%prof p=user-meta:nsur]
==
--
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